-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplatform.html
More file actions
554 lines (539 loc) · 49.4 KB
/
Copy pathplatform.html
File metadata and controls
554 lines (539 loc) · 49.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0e151f" media="(prefers-color-scheme: dark)">
<title>The MMCA Platform · Ivan Ball-llovera</title>
<meta name="description" content="MMCA is an open-source .NET 10 platform: a 15-package framework (DDD, Clean Architecture, CQRS) plus reference apps that extract a modular monolith into microservices, proven in production on Azure.">
<link rel="canonical" href="https://ivanball.github.io/platform.html">
<link rel="icon" href="assets/img/favicon.svg" type="image/svg+xml">
<link rel="alternate" type="application/rss+xml" title="Ivan Ball-llovera: deep dives on enterprise .NET" href="https://ivanball.github.io/feed.xml">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Ivan Ball-llovera">
<meta property="og:title" content="The MMCA Platform · Ivan Ball-llovera">
<meta property="og:description" content="An open-source .NET 10 framework (15 packages) plus reference apps that take a modular monolith to microservices, proven in production on Azure.">
<meta property="og:url" content="https://ivanball.github.io/platform.html">
<meta property="og:image" content="https://ivanball.github.io/assets/img/og-image.png">
<meta property="og:image:alt" content="Ivan Ball-llovera, Senior Software Architect">
<meta property="og:locale" content="en_US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="The MMCA Platform · Ivan Ball-llovera">
<meta name="twitter:description" content="An open-source .NET 10 framework (15 packages) plus reference apps that take a modular monolith to microservices, proven in production on Azure.">
<meta name="twitter:image" content="https://ivanball.github.io/assets/img/og-image.png">
<!-- BEGIN head-assets -->
<script>(function(){try{var t=localStorage.getItem('mmca-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
<link rel="preload" href="assets/fonts/inter-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="assets/fonts/jetbrains-mono-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="assets/css/styles.css">
<script defer src="assets/js/main.js"></script>
<script defer src="assets/js/search.js"></script>
<script defer src="assets/js/analytics.js"></script>
<!-- END head-assets -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
"name": "MMCA",
"alternateName": "Modular Monolith Clean Architecture",
"description": "An open-source .NET 10 framework of fifteen NuGet packages for building a modular monolith you can extract services out of later, without a rewrite: DDD, Clean Architecture, CQRS, a transactional outbox, database-per-module, and architecture fitness functions that fail the build.",
"url": "https://ivanball.github.io/platform.html",
"codeRepository": "https://github.com/ivanball/MMCA.Common",
"programmingLanguage": [{ "@type": "ComputerLanguage", "name": "C#" }],
"runtimePlatform": ".NET 10",
"license": "https://www.apache.org/licenses/LICENSE-2.0",
"author": {
"@type": "Person",
"name": "Ivan Ball-llovera",
"url": "https://ivanball.github.io/"
},
"keywords": "modular monolith, clean architecture, DDD, CQRS, microservices, .NET, Blazor, Aspire, EF Core, transactional outbox"
}
</script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<!-- BEGIN site-header -->
<header class="site-header">
<div class="container nav">
<a class="brand" href="index.html">
<span class="brand-mark" aria-hidden="true">IB</span>
<span>Ivan Ball-llovera</span>
</a>
<nav aria-label="Primary">
<ul class="nav-links" id="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="resume.html">Résumé</a></li>
<li><a href="platform.html" aria-current="page">Platform</a></li>
<li><a href="docs/index.html">Reference</a></li>
<li><a href="writing.html">Writing</a></li>
<li><a href="speaking.html">Speaking</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
</nav>
<div class="nav-tools">
<button class="icon-btn search-open" type="button" aria-label="Search the site" data-search-open>
<span aria-hidden="true">⌕</span>
</button>
<button class="icon-btn theme-toggle" type="button" aria-label="Switch color theme">
<span class="sun" aria-hidden="true">☀</span><span class="moon" aria-hidden="true">☾</span>
</button>
<button class="icon-btn nav-toggle" type="button" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="nav-links">☰</button>
</div>
</div>
</header>
<dialog class="search-dialog" id="site-search" aria-label="Search this site">
<form class="search-box" method="dialog" role="search">
<span class="search-icon" aria-hidden="true">⌕</span>
<input class="search-input" type="search" id="site-search-input" placeholder="Search ADRs, guides, chapters, articles…"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
aria-controls="site-search-results" aria-describedby="site-search-status">
<button class="btn btn--ghost search-close" type="button" data-search-close>Esc</button>
</form>
<p class="search-status" id="site-search-status" role="status" aria-live="polite"></p>
<ul class="search-results" id="site-search-results"></ul>
<p class="search-foot">
<span><kbd>↑</kbd><kbd>↓</kbd> to navigate</span>
<span><kbd>Enter</kbd> to open</span>
<span><kbd>"…"</kbd> exact phrase</span>
<span><kbd>AND</kbd><kbd>OR</kbd> to combine</span>
</p>
</dialog>
<!-- END site-header -->
<main id="main">
<!-- Intro -->
<div class="page-head">
<div class="container">
<p class="eyebrow">Featured work · Open source</p>
<h1>The MMCA platform</h1>
<p class="lede">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.</p>
<div class="btn-row">
<a class="btn btn--primary" href="docs/guides/common-GETTING-STARTED.html">Get started in 15 minutes →</a>
<a class="btn btn--ghost" href="https://github.com/ivanball/MMCA.Common" target="_blank" rel="noopener">View on GitHub ↗</a>
<a class="btn btn--ghost" href="writing.html">Read the deep dives</a>
</div>
<div class="stats">
<!-- BEGIN platform-stats -->
<div class="stat"><div class="num">15</div><div class="label">NuGet packages</div></div>
<div class="stat"><div class="num">64</div><div class="label">Architecture Decision Records</div></div>
<div class="stat"><div class="num">93</div><div class="label">Architecture fitness tests</div></div>
<div class="stat"><div class="num">3</div><div class="label">Reference applications</div></div>
<!-- END platform-stats -->
</div>
</div>
</div>
<!-- The framework -->
<section class="section section--alt">
<div class="container">
<div class="section-head">
<p class="eyebrow">The framework</p>
<h2>MMCA.Common</h2>
<p>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.</p>
</div>
<!-- GENERATED from assets/data/platform-facts.js (packageLayers). The build fails
if the package count there disagrees with the headline figure. -->
<ul class="layers">
<!-- BEGIN package-layers -->
<li class="layer">
<span class="layer-name">Shared</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.Shared</span></span>
<span class="layer-note">Result pattern, value objects, error handling, DTOs</span>
</span>
</li>
<li class="layer">
<span class="layer-name">Domain</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.Domain</span></span>
<span class="layer-note">Entities, aggregate roots, domain events, specifications</span>
</span>
</li>
<li class="layer">
<span class="layer-name">Application</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.Application</span></span>
<span class="layer-note">CQRS handlers, the decorator pipeline, the module system</span>
</span>
</li>
<li class="layer">
<span class="layer-name">Infrastructure</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.Infrastructure</span></span>
<span class="layer-note">EF Core multi-engine, repositories, caching, outbox, message bus</span>
</span>
</li>
<li class="layer layer--edge">
<span class="layer-name">API & transport</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.API</span><span class="pill">MMCA.Common.Grpc</span></span>
<span class="layer-note">Controllers, middleware, idempotency, JWKS, gRPC contracts</span>
</span>
</li>
<li class="layer">
<span class="layer-name">UI</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.UI</span><span class="pill">MMCA.Common.UI.Web</span><span class="pill">MMCA.Common.UI.Maui</span></span>
<span class="layer-note">Blazor shared components, MudBlazor theme, web and MAUI clients</span>
</span>
</li>
<li class="layer layer--edge">
<span class="layer-name">Hosting</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.Aspire</span><span class="pill">MMCA.Common.Aspire.Hosting</span></span>
<span class="layer-note">Aspire service defaults, OpenTelemetry, health checks, broker wiring</span>
</span>
</li>
<li class="layer">
<span class="layer-name">Testing</span>
<span>
<span class="pill-list"><span class="pill">MMCA.Common.Testing</span><span class="pill">MMCA.Common.Testing.Architecture</span><span class="pill">MMCA.Common.Testing.E2E</span><span class="pill">MMCA.Common.Testing.UI</span></span>
<span class="layer-note">Integration bases, architecture rule library, Playwright and bUnit harnesses</span>
</span>
</li>
<!-- END package-layers -->
</ul>
<div class="grid grid--3" style="margin-top:2rem" data-reveal>
<div class="card">
<h3>Strict layered core</h3>
<p class="mb-0">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.</p>
</div>
<div class="card">
<h3>CQRS decorator pipeline</h3>
<p class="mb-0">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.</p>
</div>
<div class="card">
<h3>Extraction boundaries</h3>
<p class="mb-0">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.</p>
</div>
</div>
</div>
</section>
<!-- Reference apps -->
<section class="section">
<div class="container">
<div class="section-head">
<p class="eyebrow">Proof, not slideware</p>
<h2>Three reference applications</h2>
<p>The framework is consumed by real apps. Each one exercises the patterns differently, and the conference app ran a live event on Azure.</p>
</div>
<div class="grid grid--3">
<div class="card">
<span class="kicker kicker--accent">Conference</span>
<h3>MMCA.ADC</h3>
<p class="mb-0">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.</p>
</div>
<div class="card">
<span class="kicker kicker--accent">E-commerce</span>
<h3>MMCA.Store</h3>
<p class="mb-0">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.</p>
</div>
<div class="card">
<span class="kicker kicker--accent">Reference seed</span>
<h3>MMCA.Helpdesk</h3>
<p class="mb-0">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.</p>
</div>
</div>
</div>
</section>
<!-- Screenshots -->
<section class="section section--alt">
<div class="container">
<div class="section-head">
<p class="eyebrow">A look inside</p>
<h2>From one graph, laptop to cloud</h2>
<p>Orchestrated locally with .NET Aspire, then deployed to Azure Container Apps from the same declarative model.</p>
</div>
<div class="figure-row" data-reveal>
<figure>
<div class="shot"><img src="assets/img/aspire-dashboard.jpg" alt="The .NET Aspire dashboard showing the MMCA services, databases, and message broker running locally" loading="lazy"></div>
<figcaption>The .NET Aspire dashboard: services, databases, and the broker as one orchestrated graph.</figcaption>
</figure>
<figure>
<div class="shot"><img src="assets/img/github-repo-readme.jpg" alt="The MMCA.Common GitHub repository README" loading="lazy"></div>
<figcaption>MMCA.Common on GitHub: Apache-2.0 licensed, documented, released in lockstep.</figcaption>
</figure>
</div>
</div>
</section>
<!-- Architecture principles -->
<section class="section">
<div class="container">
<div class="section-head">
<p class="eyebrow">How it holds together</p>
<h2>Architecture principles</h2>
<p>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.</p>
</div>
<div class="grid grid--2">
<div class="card">
<h3>Database-per-service, with an outbox</h3>
<p class="mb-0">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.</p>
</div>
<div class="card">
<h3>Cross-service auth without shared secrets</h3>
<p class="mb-0">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.</p>
</div>
<div class="card">
<h3>Soft-delete plus a right to erasure</h3>
<p class="mb-0">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.</p>
</div>
<div class="card">
<h3>Polyglot persistence behind one model</h3>
<p class="mb-0">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.</p>
</div>
</div>
</div>
</section>
<!-- Scorecard -->
<section class="section section--alt">
<div class="container">
<div class="section-head">
<p class="eyebrow">Graded honestly</p>
<h2>A two-axis architecture scorecard</h2>
<p>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.</p>
</div>
<!-- Scores are GENERATED from docs-src/governance/*-ArchitectureScorecard.md, the same files
published under /docs/governance/, so a re-score cannot leave this page stale. -->
<div class="grid grid--3" data-reveal>
<!-- BEGIN scorecards -->
<a class="meter-card card--link" href="docs/governance/common-ArchitectureScorecard.html">
<span class="meter-title"><strong>MMCA.Common</strong> <span class="meter-role">framework</span></span>
<span class="meter">
<span class="meter-label">Maturity</span>
<span class="meter-track"><span class="meter-fill" style="width:96.9%"></span></span>
<span class="meter-value">96.9%</span>
</span>
<span class="meter meter--alt">
<span class="meter-label">Implementation</span>
<span class="meter-track"><span class="meter-fill" style="width:84.8%"></span></span>
<span class="meter-value">84.8%</span>
</span>
</a>
<a class="meter-card card--link" href="docs/governance/adc-ArchitectureScorecard.html">
<span class="meter-title"><strong>MMCA.ADC</strong> <span class="meter-role">conference app</span></span>
<span class="meter">
<span class="meter-label">Maturity</span>
<span class="meter-track"><span class="meter-fill" style="width:97.2%"></span></span>
<span class="meter-value">97.2%</span>
</span>
<span class="meter meter--alt">
<span class="meter-label">Implementation</span>
<span class="meter-track"><span class="meter-fill" style="width:85.6%"></span></span>
<span class="meter-value">85.6%</span>
</span>
</a>
<a class="meter-card card--link" href="docs/governance/store-ArchitectureScorecard.html">
<span class="meter-title"><strong>MMCA.Store</strong> <span class="meter-role">e-commerce app</span></span>
<span class="meter">
<span class="meter-label">Maturity</span>
<span class="meter-track"><span class="meter-fill" style="width:97.8%"></span></span>
<span class="meter-value">97.8%</span>
</span>
<span class="meter meter--alt">
<span class="meter-label">Implementation</span>
<span class="meter-track"><span class="meter-fill" style="width:83.9%"></span></span>
<span class="meter-value">83.9%</span>
</span>
</a>
<!-- END scorecards -->
</div>
<div class="btn-row" style="margin-top:1.5rem">
<a class="btn btn--ghost" href="docs/governance/index.html">Read the full scorecards →</a>
<a class="btn btn--ghost" href="docs/governance/ArchitectureEvaluationCriteria.html">See the 34-category rubric</a>
</div>
</div>
</section>
<!-- ADRs -->
<section class="section">
<div class="container">
<div class="section-head">
<p class="eyebrow">Why, not just what</p>
<h2>Architecture Decision Records</h2>
<!-- BEGIN adr-count -->
<p>64 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.</p>
<!-- END adr-count -->
</div>
<div class="adr-list">
<!-- BEGIN adr-list -->
<a class="adr" href="docs/adr/001-manual-dto-mapping.html"><span class="adr-num">001</span><span><span class="adr-title">Manual DTO mapping</span><span class="adr-sum">Per-entity source-generated mappers chosen over reflection-based AutoMapper.</span></span></a>
<a class="adr" href="docs/adr/002-navigation-populators.html"><span class="adr-num">002</span><span><span class="adr-title">Navigation populators</span><span class="adr-sum">Cross-container and cross-source eager loading via an explicit populator contract.</span></span></a>
<a class="adr" href="docs/adr/003-outbox-dual-dispatch.html"><span class="adr-num">003</span><span><span class="adr-title">Outbox dual dispatch</span><span class="adr-sum">Outbox plus in-process dispatch plus a background processor for at-least-once delivery.</span></span></a>
<a class="adr" href="docs/adr/004-authentication-dual-fetch.html"><span class="adr-num">004</span><span><span class="adr-title">Cross-service token validation (JWKS)</span><span class="adr-sum">Extracted services validate RS256 tokens via JWKS / OIDC discovery, no shared key.</span></span></a>
<a class="adr" href="docs/adr/005-soft-delete-vs-erasure.html"><span class="adr-num">005</span><span><span class="adr-title">Soft-delete vs. erasure</span><span class="adr-sum">Soft-delete for lifecycle; anonymization plus outbox purge for GDPR/CCPA erasure.</span></span></a>
<a class="adr" href="docs/adr/006-database-per-service.html"><span class="adr-num">006</span><span><span class="adr-title">Database per service</span><span class="adr-sum">Each service owns its DB and outbox; one context class, one instance per database.</span></span></a>
<a class="adr" href="docs/adr/007-grpc-extraction.html"><span class="adr-num">007</span><span><span class="adr-title">gRPC cross-service calls</span><span class="adr-sum">Shared contracts, typed clients, and Result-over-the-wire for synchronous calls.</span></span></a>
<a class="adr" href="docs/adr/008-service-extraction-topology.html"><span class="adr-num">008</span><span><span class="adr-title">Monolith to services plus gateway</span><span class="adr-sum">One service host per module behind a YARP gateway; transport at the edge keeps it reversible.</span></span></a>
<a class="adr" href="docs/adr/009-resilience-and-recovery-objectives.html"><span class="adr-num">009</span><span><span class="adr-title">Resilience and recovery objectives</span><span class="adr-sum">A standard resilience handler on every outbound client; declared RTO/RPO and drilled restore.</span></span></a>
<a class="adr" href="docs/adr/010-integration-event-schema-versioning.html"><span class="adr-num">010</span><span><span class="adr-title">Integration-event schema versioning</span><span class="adr-sum">Every event carries a SchemaVersion; breaking changes use a new event type plus upcaster.</span></span></a>
<a class="adr" href="docs/adr/011-single-locale-i18n.html"><span class="adr-num">011</span><span><span class="adr-title">Single-locale by design</span><span class="adr-sum">Superseded by ADR-027: en-US only was a deliberate, revisitable non-goal.</span></span></a>
<a class="adr" href="docs/adr/012-grpc-host-transport.html"><span class="adr-num">012</span><span><span class="adr-title">gRPC-host transport convention</span><span class="adr-sum">Two coherent Kestrel profiles; the choice forces the gateway-forward mode and JWKS routing.</span></span></a>
<a class="adr" href="docs/adr/013-result-pattern.html"><span class="adr-num">013</span><span><span class="adr-title">Result pattern over exceptions</span><span class="adr-sum">Expected failures are Result values; only the edge maps to HTTP or gRPC status.</span></span></a>
<a class="adr" href="docs/adr/014-cqrs-decorator-pipeline.html"><span class="adr-num">014</span><span><span class="adr-title">CQRS decorator pipeline</span><span class="adr-sum">Thin handlers behind a Scrutor decorator chain whose order is load-bearing.</span></span></a>
<a class="adr" href="docs/adr/015-architecture-fitness-functions.html"><span class="adr-num">015</span><span><span class="adr-title">Architecture fitness functions</span><span class="adr-sum">Invariants gate the build twice: a compile-time layer guard plus a shared NetArchTest library.</span></span></a>
<a class="adr" href="docs/adr/016-lockstep-versioning-masstransit-pin.html"><span class="adr-num">016</span><span><span class="adr-title">Lockstep versioning + MassTransit pin</span><span class="adr-sum">All packages release at one version; the MassTransit v8 pin is a build gate.</span></span></a>
<a class="adr" href="docs/adr/017-request-idempotency.html"><span class="adr-num">017</span><span><span class="adr-title">HTTP request idempotency</span><span class="adr-sum">An attribute dedups client retries via an Idempotency-Key header and cached replay.</span></span></a>
<a class="adr" href="docs/adr/018-polyglot-persistence.html"><span class="adr-num">018</span><span><span class="adr-title">Polyglot persistence</span><span class="adr-sum">SQL Server, Cosmos, and SQLite behind one model; engine is an attribute on the config.</span></span></a>
<a class="adr" href="docs/adr/019-rate-limiting.html"><span class="adr-num">019</span><span><span class="adr-title">Layered rate limiting</span><span class="adr-sum">An always-on global limiter caps authenticated callers per user and exempts infra traffic.</span></span></a>
<a class="adr" href="docs/adr/020-permission-based-authorization.html"><span class="adr-num">020</span><span><span class="adr-title">Permission-based authorization</span><span class="adr-sum">An opt-in capability layer over RBAC; permission policies resolve on demand from a registry.</span></span></a>
<a class="adr" href="docs/adr/021-consumer-inbox-idempotency.html"><span class="adr-num">021</span><span><span class="adr-title">Consumer-side inbox idempotency</span><span class="adr-sum">An opt-in inbox dedups broker redeliveries by message id in the consumer's own database.</span></span></a>
<a class="adr" href="docs/adr/022-browser-session-cookie-auth.html"><span class="adr-num">022</span><span><span class="adr-title">Browser session-cookie auth</span><span class="adr-sum">HttpOnly cookies plus an SSR-time non-validating scheme so [Authorize] passes during prerender.</span></span></a>
<a class="adr" href="docs/adr/023-security-response-headers.html"><span class="adr-num">023</span><span><span class="adr-title">Security-response headers plus CSP</span><span class="adr-sum">Hardened security-headers middleware with a pluggable CSP that cannot break Blazor.</span></span></a>
<a class="adr" href="docs/adr/024-push-notifications.html"><span class="adr-num">024</span><span><span class="adr-title">Two-channel user notifications</span><span class="adr-sum">One use case writes a durable inbox and fires a transient SignalR push; transport is pluggable.</span></span></a>
<a class="adr" href="docs/adr/025-startup-warmup-readiness.html"><span class="adr-num">025</span><span><span class="adr-title">Startup warm-up plus readiness gating</span><span class="adr-sum">Warm-up tasks run at startup and a readiness gate holds probes off a warming replica.</span></span></a>
<a class="adr" href="docs/adr/026-caching-strategy.html"><span class="adr-num">026</span><span><span class="adr-title">Two-tier caching</span><span class="adr-sum">One swappable cache substrate (in-memory or Redis) plus an HTTP output-cache edge for public reads.</span></span></a>
<a class="adr" href="docs/adr/027-multi-locale-i18n.html"><span class="adr-num">027</span><span><span class="adr-title">Multi-locale i18n (supersedes 011)</span><span class="adr-sum">English and Spanish via resource files; backend errors localized at the edge by error code, one culture cookie across SSR, Server, and WASM.</span></span></a>
<a class="adr" href="docs/adr/028-dark-theme-mode.html"><span class="adr-num">028</span><span><span class="adr-title">Day / dark theme mode</span><span class="adr-sum">A persisted light/dark toggle bound through MudThemeProvider, defaulting to the OS preference with a no-flash cookie bootstrap.</span></span></a>
<a class="adr" href="docs/adr/029-authentication-brute-force-protection.html"><span class="adr-num">029</span><span><span class="adr-title">Brute-force login protection</span><span class="adr-sum">Email-keyed login lockout with exponential backoff plus a per-IP registration cap, covering the anonymous surface the rate limiter exempts.</span></span></a>
<a class="adr" href="docs/adr/030-startup-sole-migrator.html"><span class="adr-num">030</span><span><span class="adr-title">Startup sole-migrator</span><span class="adr-sum">Each service applies its own EF migrations at boot and is the sole migrator; no deploy-step backstop.</span></span></a>
<a class="adr" href="docs/adr/031-feature-flag-management.html"><span class="adr-num">031</span><span><span class="adr-title">Feature-flag management</span><span class="adr-sum">One flag name enforced on two surfaces: a controller gate and the outermost CQRS decorator; disabled reads as 404.</span></span></a>
<a class="adr" href="docs/adr/032-password-hashing.html"><span class="adr-num">032</span><span><span class="adr-title">Password hashing with legacy migration</span><span class="adr-sum">PBKDF2 with 600k iterations for new passwords; legacy records still verify and migrate on the owner's next password set.</span></span></a>
<a class="adr" href="docs/adr/033-resource-ownership-authorization.html"><span class="adr-num">033</span><span><span class="adr-title">Resource-ownership authorization</span><span class="adr-sum">A row-level ownership axis beside RBAC: an owner-or-admin filter rejects mismatches and a specification row-scopes queries.</span></span></a>
<a class="adr" href="docs/adr/034-generic-entity-query-layer.html"><span class="adr-num">034</span><span><span class="adr-title">Generic entity controllers</span><span class="adr-sum">Every entity inherits a REST surface plus a bounded query contract: sparse fields, typed filters, sort, and pagination.</span></span></a>
<a class="adr" href="docs/adr/035-optimistic-concurrency.html"><span class="adr-num">035</span><span><span class="adr-title">Optimistic concurrency (RowVersion)</span><span class="adr-sum">A RowVersion token round-trips through DTOs so a stale write surfaces as HTTP 409, gated by a fitness rule.</span></span></a>
<a class="adr" href="docs/adr/036-external-oauth-login.html"><span class="adr-num">036</span><span><span class="adr-title">External OAuth login</span><span class="adr-sum">Google and GitHub sign-in swap a single-use code for local JWTs; provider tokens never ride the redirect URL.</span></span></a>
<a class="adr" href="docs/adr/037-field-level-encryption-at-rest.html"><span class="adr-num">037</span><span><span class="adr-title">Field-level encryption at rest</span><span class="adr-sum">An EF converter encrypts string columns with AES-256-GCM; shipped and tested, not yet wired to an entity.</span></span></a>
<a class="adr" href="docs/adr/038-supply-chain-provenance.html"><span class="adr-num">038</span><span><span class="adr-title">Supply-chain provenance</span><span class="adr-sum">An SBOM release gate, committed lock files, a transitive vulnerability audit, and package sources pinned to nuget.org.</span></span></a>
<a class="adr" href="docs/adr/039-live-channel-push.html"><span class="adr-num">039</span><span><span class="adr-title">Live channel push</span><span class="adr-sum">Ephemeral channel events ride the existing notification hub, so one WebSocket carries durable notifications and lossy live events.</span></span></a>
<a class="adr" href="docs/adr/040-authenticated-output-caching-for-public-reads.html"><span class="adr-num">040</span><span><span class="adr-title">Authenticated output caching</span><span class="adr-sum">Public, user-independent GET endpoints cache even when requests carry a Bearer token; identity-dependent payloads never qualify.</span></span></a>
<a class="adr" href="docs/adr/041-observability-and-telemetry.html"><span class="adr-num">041</span><span><span class="adr-title">Observability and telemetry</span><span class="adr-sum">A shared OpenTelemetry baseline plus CQRS duration metrics, correlation IDs, head sampling, and outbox-poll span filtering.</span></span></a>
<a class="adr" href="docs/adr/042-device-capability-abstraction.html"><span class="adr-num">042</span><span><span class="adr-title">Device capability abstraction (MAUI)</span><span class="adr-sum">Per-capability contracts with browser fallbacks; the MAUI package overrides them for native heads (the fifteenth package).</span></span></a>
<a class="adr" href="docs/adr/043-mobile-deep-links-and-native-oauth-callback.html"><span class="adr-num">043</span><span><span class="adr-title">Mobile deep links and OAuth callback</span><span class="adr-sum">Allow-listed custom-scheme OAuth completion for MAUI plus app-association files served by each app's web host.</span></span></a>
<a class="adr" href="docs/adr/044-native-push-delivery.html"><span class="adr-num">044</span><span><span class="adr-title">Native push delivery</span><span class="adr-sum">A third notification channel: OS-level FCM/APNs push via Azure Notification Hubs reaches backgrounded and killed apps; the inbox stays the source of truth.</span></span></a>
<a class="adr" href="docs/adr/045-managed-file-storage-and-avatars.html"><span class="adr-num">045</span><span><span class="adr-title">Managed file storage and avatars</span><span class="adr-sum">Pluggable blob storage plus an image processor that strips all metadata and re-encodes uploads; avatars land as 256x256 JPEGs in a public-read container.</span></span></a>
<a class="adr" href="docs/adr/046-http-api-versioning.html"><span class="adr-num">046</span><span><span class="adr-title">HTTP API versioning strategy</span><span class="adr-sum">Header-based versioning wired in one call; supported and deprecated versions are reported on every response, with a fitness contract asserting the headers.</span></span></a>
<a class="adr" href="docs/adr/047-soft-deleted-user-session-revocation.html"><span class="adr-num">047</span><span><span class="adr-title">Soft-deleted-user session revocation</span><span class="adr-sum">A 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.</span></span></a>
<a class="adr" href="docs/adr/048-primitive-identifier-type-aliases.html"><span class="adr-num">048</span><span><span class="adr-title">Primitive identifier type aliases</span><span class="adr-sum">Entity IDs stay primitives behind per-module type aliases, chosen over strongly-typed ID structs: readable signatures with zero EF, serializer, or OpenAPI friction.</span></span></a>
<a class="adr" href="docs/adr/049-library-configureawait-policy.html"><span class="adr-num">049</span><span><span class="adr-title">Library-scoped ConfigureAwait policy</span><span class="adr-sum">Framework 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.</span></span></a>
<a class="adr" href="docs/adr/050-jwt-refresh-token-rotation.html"><span class="adr-num">050</span><span><span class="adr-title">JWT + single rotating refresh token</span><span class="adr-sum">A 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.</span></span></a>
<a class="adr" href="docs/adr/051-client-auth-token-lifecycle.html"><span class="adr-num">051</span><span><span class="adr-title">Client-side auth token lifecycle across render modes</span><span class="adr-sum">One 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.</span></span></a>
<a class="adr" href="docs/adr/052-background-job-execution.html"><span class="adr-num">052</span><span><span class="adr-title">Background job execution</span><span class="adr-sum">Work that outlives a request runs as a bounded channel plus a single-reader hosted drain, never an untracked task, so the host can cancel and await it on shutdown.</span></span></a>
<a class="adr" href="docs/adr/053-dual-registry-package-publishing.html"><span class="adr-num">053</span><span><span class="adr-title">Dual-registry package publishing</span><span class="adr-sum">Every release pushes the same packages to nuget.org and GitHub Packages from one tag, authenticated by a short-lived OIDC exchange rather than a stored key; nuget.org is the documented install path because the GitHub registry needs a token even for public packages.</span></span></a>
<a class="adr" href="docs/adr/054-saga-compensation-and-reconciliation.html"><span class="adr-num">054</span><span><span class="adr-title">Saga compensation + reconciliation backstop</span><span class="adr-sum">Cross-boundary consistency without two-phase commit: each step raises a domain event and its compensating action runs in its own scope, made idempotent by a marker committed alongside the compensating writes, with a periodic sweep as the saga-timeout backstop.</span></span></a>
<a class="adr" href="docs/adr/055-repository-and-specification-contract.html"><span class="adr-num">055</span><span><span class="adr-title">Repository + Specification data-access contract</span><span class="adr-sum">The read contract splits into id lookups and collection queries, with the raw IQueryable surfaces confined to the composite, because a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary later.</span></span></a>
<a class="adr" href="docs/adr/056-blazor-render-mode-strategy.html"><span class="adr-num">056</span><span><span class="adr-title">Blazor render-mode strategy</span><span class="adr-sum">InteractiveAuto is declared once on the root router with prerendering left on, and the resulting double fetch is cancelled once in the shared list-page base by persisting the prerendered payload rather than by turning prerendering off.</span></span></a>
<a class="adr" href="docs/adr/057-expand-contract-schema-evolution-gate.html"><span class="adr-num">057</span><span><span class="adr-title">Expand/contract schema evolution as a CI gate</span><span class="adr-sum">A migration added by a PR cannot drop a column, table, or index without an explicit override marker, because deploy rollback is revision-only and never reverts schema: the previous release has to keep running against the new one.</span></span></a>
<a class="adr" href="docs/adr/058-runtime-conformance-suites-as-a-package.html"><span class="adr-num">058</span><span><span class="adr-title">Runtime conformance suites shipped as a package</span><span class="adr-sum">Abstract contract bases that a host subclasses to prove it wired the framework's runtime contracts, asserted against a really booted host rather than by reflection over registrations. The runtime half of the fitness-function story.</span></span></a>
<a class="adr" href="docs/adr/059-module-contract-and-composition.html"><span class="adr-num">059</span><span><span class="adr-title">The IModule contract and module composition</span><span class="adr-sum">Modules are discovered by reflection and registered in topological dependency order, and a disabled module is represented by stub registrations rather than by absence: the mechanism that lets one module run as its own service.</span></span></a>
<a class="adr" href="docs/adr/060-performance-regression-gate.html"><span class="adr-num">060</span><span><span class="adr-title">Performance-regression gate</span><span class="adr-sum">A committed benchmark baseline checked in CI, gating allocations absolutely but latency only as a ratio between benchmarks, which is what keeps the gate meaningful on shared, noisy runners. A missing measurement fails rather than passes.</span></span></a>
<a class="adr" href="docs/adr/061-runtime-secret-management.html"><span class="adr-num">061</span><span><span class="adr-title">Runtime Secret Management via Key Vault References and Managed Identity</span><span class="adr-sum">Accepted (2026-08-01).</span></span></a>
<a class="adr" href="docs/adr/062-slo-alerting-as-code.html"><span class="adr-num">062</span><span><span class="adr-title">SLO Alerting as Code with an Alert-to-Runbook Build Gate</span><span class="adr-sum">Accepted (2026-08-01).</span></span></a>
<a class="adr" href="docs/adr/063-accessibility-conformance-gate.html"><span class="adr-num">063</span><span><span class="adr-title">WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate</span><span class="adr-sum">Accepted (2026-08-01).</span></span></a>
<a class="adr" href="docs/adr/064-deploy-recency-gates.html"><span class="adr-num">064</span><span><span class="adr-title">Deploy Preconditions as Proof-of-Recency Gates</span><span class="adr-sum">Accepted (2026-08-01).</span></span></a>
<!-- END adr-list -->
</div>
<div class="btn-row" style="margin-top:2rem">
<a class="btn btn--primary" href="docs/adr/index.html">Read the full ADRs →</a>
<a class="btn btn--ghost" href="https://github.com/ivanball/MMCA.Common" target="_blank" rel="noopener">View on GitHub ↗</a>
<a class="btn btn--ghost" href="writing.html">Read the deep-dive series</a>
</div>
</div>
</section>
<!-- Reference library -->
<section class="section section--alt">
<div class="container">
<div class="section-head">
<p class="eyebrow">Read the source of truth</p>
<h2>The reference library</h2>
<p>The full architecture documentation, maintained in this site's repository as its canonical home and rendered as browsable pages: every Architecture Decision Record, the governance scorecards, the guides and specifications, and the complete onboarding guide that walks the codebase type by type.</p>
</div>
<!-- Cards are GENERATED from the docs-src/ collections by tools/build-docs.mjs, so their
counts cannot drift from what is actually published. -->
<div class="grid grid--2">
<!-- BEGIN library-cards -->
<a class="card card--link" href="docs/adr/index.html">
<span class="kicker kicker--accent">64 records</span>
<h3>Architecture Decision Records</h3>
<p>The context, decision, rationale, and trade-offs behind every cross-cutting pattern, from manual DTO mapping and the outbox to JWKS auth, caching, and supply-chain provenance. Numbered, dated, and cross-linked.</p>
<div class="card-foot"><span class="go">Browse the ADRs →</span></div>
</a>
<a class="card card--link" href="docs/onboarding/index.html">
<span class="kicker kicker--accent">38 documents</span>
<h3>Onboarding guide</h3>
<p>A teaching guide for an engineer new to the codebase: a primer, a mechanically extracted type inventory, 27 group chapters walking every first-party type, five DevOps chapters, concept maps, and a coverage audit.</p>
<div class="card-foot"><span class="go">Open the guide →</span></div>
</a>
<a class="card card--link" href="docs/governance/index.html">
<span class="kicker kicker--accent">7 artifacts</span>
<h3>Architecture governance</h3>
<p>The 34-category evaluation rubric, plus an evidence-based scorecard and remediation backlog for each repo. Every score cites the code that earns it.</p>
<div class="card-foot"><span class="go">Read the scorecards →</span></div>
</a>
<a class="card card--link" href="docs/guides/index.html">
<span class="kicker kicker--accent">14 guides</span>
<h3>Guides & specifications</h3>
<p>The narrative layer: the getting-started guide for adopting the framework, business specifications and workflow analyses for both applications, and per-concern reference notes.</p>
<div class="card-foot"><span class="go">Browse the guides →</span></div>
</a>
<!-- END library-cards -->
</div>
<div class="btn-row" style="margin-top:1.75rem">
<a class="btn btn--ghost" href="docs/index.html">Open the reference library →</a>
</div>
</div>
</section>
<!-- Start here -->
<section class="section">
<div class="container">
<div class="section-head">
<p class="eyebrow">Start here</p>
<h2>Use it, read it, or follow along</h2>
<p>The framework is Apache 2.0 and published to nuget.org. The fastest way in is the reference app: one module wired end to end through all five layers, with the extraction path already in place.</p>
</div>
<div class="grid grid--3">
<div class="card">
<h3>Try the reference app</h3>
<p>MMCA.Helpdesk is a runnable seed built against the framework, and the worked companion to the getting-started guide. Every step in the guide maps to real code in that repo.</p>
<div class="card-foot"><a href="docs/guides/common-GETTING-STARTED.html">Getting started →</a></div>
</div>
<div class="card">
<h3>Install the packages</h3>
<p>Fifteen packages, released in lockstep at one version. One reference is usually enough to start: each package brings the layers below it.</p>
<div class="card-foot"><a href="https://www.nuget.org/packages/MMCA.Common.API" target="_blank" rel="noopener">MMCA.Common.API on nuget.org ↗</a></div>
</div>
<div class="card">
<h3>Read the code</h3>
<p>The framework and the reference app are public. The two production applications that track it are the source of the case-study material in the docs.</p>
<div class="card-foot"><a href="https://github.com/ivanball/MMCA.Common" target="_blank" rel="noopener">MMCA.Common on GitHub ↗</a></div>
</div>
</div>
<div class="btn-row" style="margin-top:1.75rem">
<a class="btn btn--primary" href="https://medium.com/@ivanball76" target="_blank" rel="noopener">Follow the series on Medium ↗</a>
<a class="btn btn--ghost" href="writing.html">Browse all articles</a>
<a class="btn btn--ghost" href="https://github.com/ivanball/MMCA.Helpdesk" target="_blank" rel="noopener">MMCA.Helpdesk ↗</a>
</div>
<!-- BEGIN subscribe -->
<div class="subscribe" data-newsletter-wrap hidden>
<h2 style="margin:0 0 0.4rem">Get each deep dive by email</h2>
<p class="mb-0" style="max-width:60ch">One message per article, no digests and no other mail.</p>
<form class="subscribe-form" data-newsletter method="post" target="_blank">
<label class="sr-only" for="platform-subscribe-email">Email address</label>
<input id="platform-subscribe-email" type="email" name="email" required placeholder="you@example.com" autocomplete="email">
<button class="btn btn--primary" type="submit">Subscribe</button>
</form>
</div>
<!-- END subscribe -->
</div>
</section>
</main>
<!-- BEGIN site-footer -->
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<p class="footer-meta mb-0"><strong>Ivan Ball-llovera</strong> · Senior Software Architect · Douglasville, GA</p>
<ul class="footer-links">
<li><a href="resume.html">Résumé</a></li>
<li><a href="platform.html">Platform</a></li>
<li><a href="docs/index.html">Reference</a></li>
<li><a href="writing.html">Writing</a></li>
<li><a href="speaking.html">Speaking</a></li>
<li><a href="contact.html">Contact</a></li>
<li><a href="https://github.com/ivanball" target="_blank" rel="me noopener">GitHub</a></li>
<li><a href="https://www.linkedin.com/in/ivan-ball-llovera-6549a911" target="_blank" rel="me noopener">LinkedIn</a></li>
</ul>
</div>
<p class="footer-meta" style="margin-top:1rem">© <span class="js-year">2026</span> Ivan Ball-llovera. Built as a static site.</p>
</div>
</footer>
<!-- END site-footer -->
</body>
</html>