Featured work · Open source

The MMCA platform

A production-grade .NET 10 platform that demonstrates modern enterprise architecture end to end: an open-source framework of fifteen NuGet packages plus real applications that prove the patterns under live traffic. The design goal is one sentence: build the monolith now, extract a service later, without a rewrite.

15
NuGet packages
51
Architecture Decision Records
91
Architecture fitness tests
3
Reference applications

The framework

MMCA.Common

A NuGet package framework for building modular-monolith applications with DDD, Clean Architecture, and CQRS, and the extension points to extract a module into its own microservice later. Fifteen packages release in lockstep and span every layer.

  • MMCA.Common.Shared
  • MMCA.Common.Domain
  • MMCA.Common.Application
  • MMCA.Common.Infrastructure
  • MMCA.Common.API
  • MMCA.Common.Grpc
  • MMCA.Common.UI
  • MMCA.Common.Aspire
  • MMCA.Common.Aspire.Hosting
  • MMCA.Common.Testing
  • MMCA.Common.Testing.Architecture
  • MMCA.Common.Testing.E2E
  • MMCA.Common.Testing.UI

Strict layered core

Shared → Domain → Application → Infrastructure → API/Grpc. The Result pattern carries expected failures, entities are rich aggregates with factory methods, and queries compose from specifications instead of LINQ spaghetti.

CQRS decorator pipeline

Thin command and query handlers wrapped by a Scrutor decorator chain: FeatureGate → Logging → Caching → Validating → Transactional → Handler. The order is load-bearing and registered once.

Extraction boundaries

Application code talks to abstractions; transport lives at the edge. A transport-agnostic message bus, gRPC contracts, JWKS cross-service auth, and Aspire hosting let a module become a service with no domain rewrite.

Proof, not slideware

Three reference applications

The framework is consumed by real apps. Each one exercises the patterns differently, and the conference app ran a live event on Azure.

Conference

MMCA.ADC

A production-deployed conference platform: four microservices (Identity, Conference, Engagement, Notification) behind a YARP gateway, cross-service JWKS auth, bidirectional gRPC, polyglot persistence, and load testing at conference-day scale. It powered the Atlanta Cloud + AI Conference.

E-commerce

MMCA.Store

An e-commerce app refactored from a monolith into three microservices (Catalog, Sales, Identity) with Stripe payments, output caching, role-based access control, and an accessible Blazor and .NET MAUI UI.

Reference seed

MMCA.Helpdesk

The deliberately minimal seed: a single Tickets module exercised through all five layers. It is the worked companion to the framework's getting-started guide and the easiest entry point for exploring the patterns.

A look inside

From one graph, laptop to cloud

Orchestrated locally with .NET Aspire, then deployed to Azure Container Apps from the same declarative model.

The .NET Aspire dashboard showing the MMCA services, databases, and message broker running locally
The .NET Aspire dashboard: services, databases, and the broker as one orchestrated graph.
The MMCA.Common GitHub repository README
MMCA.Common on GitHub: Apache-2.0 licensed, documented, released in lockstep.

How it holds together

Architecture principles

The same invariants are enforced across every repo, twice: a compile-time MSBuild layer guard and a shared NetArchTest rule library run as fitness functions.

Database-per-service, with an outbox

Each module or service owns its own database and outbox. Domain events are persisted atomically with the data, then delivered at least once, in-process for the monolith or over a broker once extracted. The outbox is the cross-source consistency mechanism.

Cross-service auth without shared secrets

Extracted services validate the issuer's RS256 tokens via JWKS / OIDC discovery, routed through the gateway with a direct fallback. No symmetric secret crosses a service boundary.

Soft-delete plus a right to erasure

Entities are soft-deleted for lifecycle and never hard-deleted; an anonymization pathway plus outbox purge satisfies GDPR/CCPA erasure. Audit fields are stamped automatically on every save.

Polyglot persistence behind one model

SQL Server, Cosmos DB, and SQLite sit behind a single entity model; the engine is an attribute on the configuration, orthogonal to the database-name axis. Plumbing shipped and tested.

Graded honestly

A two-axis architecture scorecard

Every repo is scored against a 34-category rubric on two axes, Maturity (how complete the decision is) and Implementation (how well it is realized in code), with evidence and recorded gaps rather than a single vanity number.

MMCA.Common (framework)

Maturity 96.3% · Implementation 84.6%
Across all 34 categories; i18n is scored since multi-locale support shipped.

MMCA.ADC (conference app)

Maturity 97.8% · Implementation 86.3%
Scored the same way, then remediated and re-scored as the apps evolved.

Why, not just what

Architecture Decision Records

Forty-nine ADRs capture the context and trade-offs behind each cross-cutting pattern, so the design is teachable, not tribal knowledge. Every entry below links to the full record.

001Manual DTO mappingPer-entity source-generated mappers chosen over reflection-based AutoMapper. 002Navigation populatorsCross-container and cross-source eager loading via an explicit populator contract. 003Outbox dual dispatchOutbox plus in-process dispatch plus a background processor for at-least-once delivery. 004Cross-service token validation (JWKS)Extracted services validate RS256 tokens via JWKS / OIDC discovery, no shared key. 005Soft-delete vs. erasureSoft-delete for lifecycle; anonymization plus outbox purge for GDPR/CCPA erasure. 006Database per serviceEach service owns its DB and outbox; one context class, one instance per database. 007gRPC cross-service callsShared contracts, typed clients, and Result-over-the-wire for synchronous calls. 008Monolith to services plus gatewayOne service host per module behind a YARP gateway; transport at the edge keeps it reversible. 009Resilience and recovery objectivesA standard resilience handler on every outbound client; declared RTO/RPO and drilled restore. 010Integration-event schema versioningEvery event carries a SchemaVersion; breaking changes use a new event type plus upcaster. 011Single-locale by designSuperseded by ADR-027: en-US only was a deliberate, revisitable non-goal. 012gRPC-host transport conventionTwo coherent Kestrel profiles; the choice forces the gateway-forward mode and JWKS routing. 013Result pattern over exceptionsExpected failures are Result values; only the edge maps to HTTP or gRPC status. 014CQRS decorator pipelineThin handlers behind a Scrutor decorator chain whose order is load-bearing. 015Architecture fitness functionsInvariants gate the build twice: a compile-time layer guard plus a shared NetArchTest library. 016Lockstep versioning + MassTransit pinAll packages release at one version; the MassTransit v8 pin is a build gate. 017HTTP request idempotencyAn attribute dedups client retries via an Idempotency-Key header and cached replay. 018Polyglot persistenceSQL Server, Cosmos, and SQLite behind one model; engine is an attribute on the config. 019Layered rate limitingAn always-on global limiter caps authenticated callers per user and exempts infra traffic. 020Permission-based authorizationAn opt-in capability layer over RBAC; permission policies resolve on demand from a registry. 021Consumer-side inbox idempotencyAn opt-in inbox dedups broker redeliveries by message id in the consumer's own database. 022Browser session-cookie authHttpOnly cookies plus an SSR-time non-validating scheme so [Authorize] passes during prerender. 023Security-response headers plus CSPHardened security-headers middleware with a pluggable CSP that cannot break Blazor. 024Two-channel user notificationsOne use case writes a durable inbox and fires a transient SignalR push; transport is pluggable. 025Startup warm-up plus readiness gatingWarm-up tasks run at startup and a readiness gate holds probes off a warming replica. 026Two-tier cachingOne swappable cache substrate (in-memory or Redis) plus an HTTP output-cache edge for public reads. 027Multi-locale i18n (supersedes 011)English and Spanish via resource files; backend errors localized at the edge by error code, one culture cookie across SSR, Server, and WASM. 028Day / dark theme modeA persisted light/dark toggle bound through MudThemeProvider, defaulting to the OS preference with a no-flash cookie bootstrap. 029Brute-force login protectionEmail-keyed login lockout with exponential backoff plus a per-IP registration cap, covering the anonymous surface the rate limiter exempts. 030Startup sole-migratorEach service applies its own EF migrations at boot and is the sole migrator; no deploy-step backstop. 031Feature-flag managementOne flag name enforced on two surfaces: a controller gate and the outermost CQRS decorator; disabled reads as 404. 032Password hashing with legacy migrationPBKDF2 with 600k iterations for new passwords; legacy records still verify and migrate on the owner's next password set. 033Resource-ownership authorizationA row-level ownership axis beside RBAC: an owner-or-admin filter rejects mismatches and a specification row-scopes queries. 034Generic entity controllersEvery entity inherits a REST surface plus a bounded query contract: sparse fields, typed filters, sort, and pagination. 035Optimistic concurrency (RowVersion)A RowVersion token round-trips through DTOs so a stale write surfaces as HTTP 409, gated by a fitness rule. 036External OAuth loginGoogle and GitHub sign-in swap a single-use code for local JWTs; provider tokens never ride the redirect URL. 037Field-level encryption at restAn EF converter encrypts string columns with AES-256-GCM; shipped and tested, not yet wired to an entity. 038Supply-chain provenanceAn SBOM release gate, committed lock files, a transitive vulnerability audit, and package sources pinned to nuget.org. 039Live channel pushEphemeral channel events ride the existing notification hub, so one WebSocket carries durable notifications and lossy live events. 040Authenticated output cachingPublic, user-independent GET endpoints cache even when requests carry a Bearer token; identity-dependent payloads never qualify. 041Observability and telemetryA shared OpenTelemetry baseline plus CQRS duration metrics, correlation IDs, head sampling, and outbox-poll span filtering. 042Device capability abstraction (MAUI)Per-capability contracts with browser fallbacks; the MAUI package overrides them for native heads (the fifteenth package). 043Mobile deep links and OAuth callbackAllow-listed custom-scheme OAuth completion for MAUI plus app-association files served by each app's web host. 044Native push deliveryA third notification channel: OS-level FCM/APNs push via Azure Notification Hubs reaches backgrounded and killed apps; the inbox stays the source of truth. 045Managed file storage and avatarsPluggable blob storage plus an image processor that strips all metadata and re-encodes uploads; avatars land as 256x256 JPEGs in a public-read container. 046HTTP API versioning strategyHeader-based versioning wired in one call; supported and deprecated versions are reported on every response, with a fitness contract asserting the headers. 047Soft-deleted-user session revocationA middleware returns 401 for authenticated callers whose account is soft-deleted, bounding the stateless-JWT revocation window to a 30-second cache instead of the token lifetime. 048Primitive identifier type aliasesEntity IDs stay primitives behind per-module type aliases, chosen over strongly-typed ID structs: readable signatures with zero EF, serializer, or OpenAPI friction. 049Library-scoped ConfigureAwait policyFramework packages await with ConfigureAwait(false), enforced by CA2007 at error severity; application repos keep the analyzer off because ASP.NET Core has no synchronization context. 050JWT + single rotating refresh tokenA short-lived stateless JWT plus one server-stored, opaque refresh token per user that rotates on every use; each rotation slides a fixed inactivity window, and a mismatch revokes the chain. 051Client-side auth token lifecycle across render modesOne ITokenRefresher abstraction with head-specific strategies: browser heads refresh through the same-origin proxy cookie, MAUI refreshes directly and persists the rotated pair in OS SecureStorage.