@jeffrey2423/coding-standards 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +95 -174
  2. package/bin/cli.js +373 -20
  3. package/package.json +13 -3
  4. package/standards/backend/architecture/event-driven.md +112 -0
  5. package/standards/backend/architecture/microservice-anatomy.md +106 -0
  6. package/standards/backend/architecture/multitenancy.md +112 -0
  7. package/standards/backend/architecture/public-api-facade.md +112 -0
  8. package/standards/backend/architecture/shared-vs-owned.md +62 -0
  9. package/standards/{backend-standards.md → backend/backend-standards.md} +8 -1
  10. package/standards/{database-conventions.md → backend/database-conventions.md} +7 -0
  11. package/standards/backend/technology-stack.md +73 -0
  12. package/standards/core/ai-collaboration.md +64 -0
  13. package/standards/core/clean-architecture-ddd.md +69 -0
  14. package/standards/core/coding-conventions.md +66 -0
  15. package/standards/core/testing-strategy.md +46 -0
  16. package/standards/{mobile-flutter-standards.md → mobile/flutter/flutter-standards.md} +9 -1
  17. package/standards/{mobile-react-native-standards.md → mobile/react-native/react-native-standards.md} +9 -1
  18. package/standards/{technical-preferences-ux.md → web/_base/design-system-ux.md} +8 -1
  19. package/standards/web/_base/frontend-architecture.md +75 -0
  20. package/standards/{frontend-standards.md → web/_base/frontend-standards.md} +7 -0
  21. package/standards/web/_base/technology-stack.md +40 -0
  22. package/standards/web/microfrontends/module-federation-standard.md +216 -0
  23. package/standards/web/single-spa/single-spa-standard.md +196 -0
  24. package/standards/web/spa/spa-standard.md +53 -0
  25. package/standards/architecture-patterns.md +0 -444
  26. package/standards/technology-stack.md +0 -294
  27. package/standards/vite-config-standard.md +0 -531
package/bin/cli.js CHANGED
@@ -1,35 +1,388 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Interactive installer for the coding-standards library.
4
+ // Lets the user pick exactly the standards their project needs (by platform /
5
+ // architecture) and copies only those into ./coding-standards, then generates
6
+ // an INDEX.md so AI coding agents know which standards are active.
7
+
3
8
  const fs = require("fs");
4
9
  const path = require("path");
5
10
 
6
- const standardsDir = path.join(__dirname, "..", "standards");
7
- const targetDir = path.join(process.cwd(), "coding-standards");
11
+ const STANDARDS_DIR = path.join(__dirname, "..", "standards");
12
+ const TARGET_DIR = path.join(process.cwd(), "coding-standards");
13
+ const MANIFEST = path.join(TARGET_DIR, ".standards-manifest.json");
8
14
 
9
- const files = fs
10
- .readdirSync(standardsDir)
11
- .filter((f) => f.endsWith(".md"));
15
+ // The exact flat file set shipped by v1.x. Used to clean up a v1 install on
16
+ // upgrade without touching the user's own root-level files.
17
+ const LEGACY_V1_FILES = new Set([
18
+ "architecture-patterns.md",
19
+ "backend-standards.md",
20
+ "database-conventions.md",
21
+ "frontend-standards.md",
22
+ "mobile-flutter-standards.md",
23
+ "mobile-react-native-standards.md",
24
+ "technical-preferences-ux.md",
25
+ "technology-stack.md",
26
+ "vite-config-standard.md",
27
+ ]);
12
28
 
13
- if (files.length === 0) {
14
- console.error("No standard files found in package.");
15
- process.exit(1);
29
+ // ── Selectable architecture docs (backend, opt-in) ───────────────────────────
30
+ const ARCH_DOCS = [
31
+ { id: "anatomy", file: "microservice-anatomy.md", label: "Microservice anatomy (layers, events)" },
32
+ { id: "multitenancy", file: "multitenancy.md", label: "Multi-tenancy (RLS, tenant catalog)" },
33
+ { id: "events", file: "event-driven.md", label: "Event-driven (outbox, sagas)" },
34
+ { id: "api", file: "public-api-facade.md", label: "Public API facade (gateway, webhooks)" },
35
+ { id: "shared", file: "shared-vs-owned.md", label: "Shared vs owned components" },
36
+ ];
37
+
38
+ // ── Resolve which directories/files to copy from a selection ─────────────────
39
+ function resolveSources(sel) {
40
+ const sources = []; // { from: absolute path, to: relative path under coding-standards }
41
+
42
+ // core is always included
43
+ sources.push({ dir: "core" });
44
+
45
+ if (sel.backend) {
46
+ // base backend docs
47
+ for (const f of ["backend-standards.md", "technology-stack.md", "database-conventions.md"]) {
48
+ sources.push({ file: path.join("backend", f) });
49
+ }
50
+ // opt-in architecture docs
51
+ for (const a of ARCH_DOCS) {
52
+ if (sel.arch.includes(a.id)) {
53
+ sources.push({ file: path.join("backend", "architecture", a.file) });
54
+ }
55
+ }
56
+ }
57
+
58
+ if (sel.web) {
59
+ sources.push({ dir: path.join("web", "_base") });
60
+ sources.push({ dir: path.join("web", sel.web) }); // spa | single-spa | microfrontends
61
+ }
62
+
63
+ for (const fw of sel.mobile) {
64
+ sources.push({ dir: path.join("mobile", fw) }); // flutter | react-native
65
+ }
66
+
67
+ return sources;
68
+ }
69
+
70
+ // ── Copy helpers ─────────────────────────────────────────────────────────────
71
+ function listMarkdown(absDir) {
72
+ const out = [];
73
+ for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
74
+ const abs = path.join(absDir, entry.name);
75
+ if (entry.isDirectory()) out.push(...listMarkdown(abs));
76
+ else if (entry.name.endsWith(".md")) out.push(abs);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ function copyFile(absFrom) {
82
+ const rel = path.relative(STANDARDS_DIR, absFrom);
83
+ const dest = path.join(TARGET_DIR, rel);
84
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
85
+ fs.copyFileSync(absFrom, dest);
86
+ return rel.split(path.sep).join("/");
87
+ }
88
+
89
+ function copySources(sources) {
90
+ const copied = [];
91
+ for (const s of sources) {
92
+ if (s.dir) {
93
+ const abs = path.join(STANDARDS_DIR, s.dir);
94
+ if (!fs.existsSync(abs)) continue;
95
+ for (const f of listMarkdown(abs)) copied.push(copyFile(f));
96
+ } else if (s.file) {
97
+ const abs = path.join(STANDARDS_DIR, s.file);
98
+ if (!fs.existsSync(abs)) continue;
99
+ copied.push(copyFile(abs));
100
+ }
101
+ }
102
+ return [...new Set(copied)].sort();
103
+ }
104
+
105
+ // ── Upgrade / re-run cleanup ─────────────────────────────────────────────────
106
+ // Remove files this installer created on a previous run (tracked in the
107
+ // manifest) plus any v1 flat-layout leftovers, so upgrading from v1 or changing
108
+ // the selection never leaves stale, contradictory standards behind. Files the
109
+ // installer doesn't own are never touched.
110
+ function cleanPreviousInstall() {
111
+ if (!fs.existsSync(TARGET_DIR)) return { removed: 0, legacy: 0 };
112
+ let removed = 0;
113
+ let legacy = 0;
114
+
115
+ // 1. Files tracked by a previous v2 run.
116
+ if (fs.existsSync(MANIFEST)) {
117
+ try {
118
+ const prev = JSON.parse(fs.readFileSync(MANIFEST, "utf8"));
119
+ for (const rel of prev.files || []) {
120
+ const abs = path.join(TARGET_DIR, rel);
121
+ if (fs.existsSync(abs)) { fs.rmSync(abs); removed++; }
122
+ }
123
+ } catch {
124
+ /* corrupt manifest — fall through to legacy detection */
125
+ }
126
+ }
127
+
128
+ // 2. v1 leftovers: remove only the exact flat files v1 shipped — never the
129
+ // user's own root-level markdown.
130
+ for (const entry of fs.readdirSync(TARGET_DIR, { withFileTypes: true })) {
131
+ if (entry.isFile() && LEGACY_V1_FILES.has(entry.name)) {
132
+ fs.rmSync(path.join(TARGET_DIR, entry.name));
133
+ legacy++;
134
+ }
135
+ }
136
+
137
+ pruneEmptyDirs(TARGET_DIR);
138
+ return { removed, legacy };
139
+ }
140
+
141
+ function pruneEmptyDirs(dir) {
142
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
143
+ if (!entry.isDirectory()) continue;
144
+ const sub = path.join(dir, entry.name);
145
+ pruneEmptyDirs(sub);
146
+ if (fs.readdirSync(sub).length === 0) fs.rmdirSync(sub);
147
+ }
148
+ }
149
+
150
+ function writeManifest(copied, sel) {
151
+ const data = {
152
+ version: require("../package.json").version,
153
+ selection: describeSelection(sel),
154
+ files: copied,
155
+ };
156
+ fs.writeFileSync(MANIFEST, JSON.stringify(data, null, 2) + "\n");
157
+ }
158
+
159
+ // ── Front-matter parsing for INDEX generation ────────────────────────────────
160
+ function readFrontMatter(relPath) {
161
+ const abs = path.join(TARGET_DIR, relPath);
162
+ const text = fs.readFileSync(abs, "utf8");
163
+ const m = text.match(/^---\s*([\s\S]*?)\s*---/);
164
+ const fm = { title: "", load_when: "" };
165
+ if (m) {
166
+ for (const line of m[1].split("\n")) {
167
+ const t = line.match(/^title:\s*(.+)$/);
168
+ const l = line.match(/^load_when:\s*"?(.+?)"?\s*$/);
169
+ if (t) fm.title = t[1].trim();
170
+ if (l) fm.load_when = l[1].trim();
171
+ }
172
+ }
173
+ if (!fm.title) {
174
+ // fall back to first H1 or filename
175
+ const h1 = text.match(/^#\s+(.+)$/m);
176
+ fm.title = h1 ? h1[1].trim() : path.basename(relPath, ".md");
177
+ }
178
+ return fm;
179
+ }
180
+
181
+ function generateIndex(copied, sel) {
182
+ const lines = [];
183
+ lines.push("# Coding Standards — Active Set");
184
+ lines.push("");
185
+ lines.push("> Generated by `@jeffrey2423/coding-standards`. These are the standards active in THIS project.");
186
+ lines.push("> AI agents: read this first, then load a standard on demand per its **Load when** trigger.");
187
+ lines.push("");
188
+ lines.push(`Selection: ${describeSelection(sel)}`);
189
+ lines.push("");
190
+ lines.push("| Standard | File | Load when |");
191
+ lines.push("|---|---|---|");
192
+ for (const rel of copied) {
193
+ const fm = readFrontMatter(rel);
194
+ const lw = fm.load_when || "—";
195
+ lines.push(`| ${fm.title} | \`${rel}\` | ${lw} |`);
196
+ }
197
+ lines.push("");
198
+ lines.push("## Precedence");
199
+ lines.push("");
200
+ lines.push("- A more specific platform/track doc overrides a general one.");
201
+ lines.push("- `MUST` overrides `SHOULD`. Surface real conflicts to the user instead of guessing.");
202
+ lines.push("");
203
+ fs.writeFileSync(path.join(TARGET_DIR, "INDEX.md"), lines.join("\n"));
204
+ }
205
+
206
+ function describeSelection(sel) {
207
+ const parts = [];
208
+ if (sel.backend) parts.push(`backend (arch: ${sel.arch.length ? sel.arch.join(", ") : "none"})`);
209
+ if (sel.web) parts.push(`web/${sel.web}`);
210
+ if (sel.mobile.length) parts.push(`mobile/${sel.mobile.join("+")}`);
211
+ return parts.length ? parts.join("; ") : "core only";
212
+ }
213
+
214
+ // ── Flag parsing (non-interactive mode) ──────────────────────────────────────
215
+ function parseFlags(argv) {
216
+ const flags = {};
217
+ for (const arg of argv) {
218
+ if (!arg.startsWith("--")) continue;
219
+ const [k, v] = arg.slice(2).split("=");
220
+ flags[k] = v === undefined ? true : v;
221
+ }
222
+ return flags;
223
+ }
224
+
225
+ function selectionFromFlags(flags) {
226
+ const all = flags.all === true;
227
+ const sel = { backend: false, web: null, mobile: [], arch: [] };
228
+
229
+ if (all) {
230
+ sel.backend = true;
231
+ sel.web = "microfrontends";
232
+ sel.mobile = ["flutter", "react-native"];
233
+ sel.arch = ARCH_DOCS.map((a) => a.id);
234
+ return sel;
235
+ }
236
+
237
+ if (flags.backend) sel.backend = true;
238
+ if (typeof flags.web === "string") sel.web = flags.web;
239
+ else if (flags.web === true) sel.web = "spa";
240
+ if (typeof flags.mobile === "string") sel.mobile = flags.mobile.split(",").map((s) => s.trim());
241
+
242
+ if (sel.backend) {
243
+ if (flags["no-arch"]) sel.arch = [];
244
+ else if (typeof flags.arch === "string" && flags.arch !== "all") {
245
+ sel.arch = flags.arch.split(",").map((s) => s.trim()).filter((id) => ARCH_DOCS.some((a) => a.id === id));
246
+ } else {
247
+ sel.arch = ARCH_DOCS.map((a) => a.id); // default: all
248
+ }
249
+ }
250
+ return sel;
251
+ }
252
+
253
+ function hasPlatformFlag(flags) {
254
+ return flags.all || flags.backend || flags.web !== undefined || flags.mobile !== undefined;
255
+ }
256
+
257
+ // ── Interactive mode (@clack/prompts) ────────────────────────────────────────
258
+ async function interactive() {
259
+ const p = await import("@clack/prompts");
260
+ p.intro("📐 coding-standards — pick what your project needs");
261
+
262
+ const platforms = await p.multiselect({
263
+ message: "What are you building? (space to toggle, enter to confirm)",
264
+ options: [
265
+ { value: "backend", label: "Backend / API (.NET)" },
266
+ { value: "web", label: "Frontend Web" },
267
+ { value: "mobile", label: "Mobile" },
268
+ ],
269
+ required: false,
270
+ });
271
+ if (p.isCancel(platforms)) cancel(p);
272
+
273
+ const sel = { backend: false, web: null, mobile: [], arch: [] };
274
+ sel.backend = platforms.includes("backend");
275
+
276
+ if (platforms.includes("web")) {
277
+ const web = await p.select({
278
+ message: "Web architecture:",
279
+ options: [
280
+ { value: "spa", label: "SPA (single deployable)", hint: "default" },
281
+ { value: "single-spa", label: "Single-SPA", hint: "mixed frameworks / hard isolation" },
282
+ { value: "microfrontends", label: "Microfrontends (Module Federation)", hint: "license-gated, multi-product" },
283
+ ],
284
+ initialValue: "spa",
285
+ });
286
+ if (p.isCancel(web)) cancel(p);
287
+ sel.web = web;
288
+ }
289
+
290
+ if (platforms.includes("mobile")) {
291
+ const mobile = await p.multiselect({
292
+ message: "Mobile framework:",
293
+ options: [
294
+ { value: "flutter", label: "Flutter" },
295
+ { value: "react-native", label: "React Native" },
296
+ ],
297
+ required: true,
298
+ });
299
+ if (p.isCancel(mobile)) cancel(p);
300
+ sel.mobile = mobile;
301
+ }
302
+
303
+ if (sel.backend) {
304
+ const arch = await p.multiselect({
305
+ message: "Backend — distributed architecture standards (opt-in):",
306
+ options: ARCH_DOCS.map((a) => ({ value: a.id, label: a.label })),
307
+ initialValues: ARCH_DOCS.map((a) => a.id),
308
+ required: false,
309
+ });
310
+ if (p.isCancel(arch)) cancel(p);
311
+ sel.arch = arch;
312
+ }
313
+
314
+ return sel;
16
315
  }
17
316
 
18
- const existed = fs.existsSync(targetDir);
19
- if (existed) {
20
- console.log("coding-standards/ already exists — overwriting files.\n");
317
+ function cancel(p) {
318
+ p.cancel("Cancelled — nothing was written.");
319
+ process.exit(0);
21
320
  }
22
321
 
23
- fs.mkdirSync(targetDir, { recursive: true });
322
+ // ── Help ─────────────────────────────────────────────────────────────────────
323
+ function printHelp() {
324
+ console.log(`
325
+ coding-standards — copy modern, AI-ready coding standards into your project.
24
326
 
25
- for (const file of files) {
26
- fs.copyFileSync(path.join(standardsDir, file), path.join(targetDir, file));
327
+ Usage:
328
+ npx @jeffrey2423/coding-standards interactive
329
+ npx @jeffrey2423/coding-standards [flags] non-interactive
330
+
331
+ Flags:
332
+ --backend include .NET backend standards
333
+ --web[=track] include web standards (track: spa | single-spa | microfrontends; default spa)
334
+ --mobile=flutter,react-native include mobile standards (comma-separated)
335
+ --arch=a,b,... | --no-arch backend architecture docs (default: all). ids: ${ARCH_DOCS.map((a) => a.id).join(", ")}
336
+ --all include everything
337
+ --yes, -y run non-interactively with whatever flags are given
338
+ --help, -h show this help
339
+
340
+ Examples:
341
+ npx @jeffrey2423/coding-standards --backend --web=microfrontends
342
+ npx @jeffrey2423/coding-standards --mobile=flutter --yes
343
+ npx @jeffrey2423/coding-standards --all
344
+ `);
27
345
  }
28
346
 
29
- console.log(
30
- `Copied ${files.length} coding standard files to coding-standards/:\n`
31
- );
32
- for (const file of files) {
33
- console.log(` - ${file}`);
347
+ // ── Main ─────────────────────────────────────────────────────────────────────
348
+ async function main() {
349
+ const argv = process.argv.slice(2);
350
+ const flags = parseFlags(argv);
351
+
352
+ if (flags.help || argv.includes("-h")) {
353
+ printHelp();
354
+ return;
355
+ }
356
+
357
+ let sel;
358
+ const nonInteractive = hasPlatformFlag(flags) || flags.yes || argv.includes("-y");
359
+ if (nonInteractive) {
360
+ sel = selectionFromFlags(flags);
361
+ } else {
362
+ sel = await interactive();
363
+ }
364
+
365
+ const sources = resolveSources(sel);
366
+ const upgrading = fs.existsSync(TARGET_DIR);
367
+ fs.mkdirSync(TARGET_DIR, { recursive: true });
368
+
369
+ // Idempotent: clear what we previously owned (and any v1 leftovers) first.
370
+ const { removed, legacy } = cleanPreviousInstall();
371
+ if (upgrading && (removed || legacy)) {
372
+ const note = legacy ? ` (incl. ${legacy} from a previous flat-layout v1 install)` : "";
373
+ console.log(`Existing install detected — removed ${removed + legacy} stale file(s)${note}.`);
374
+ }
375
+
376
+ const copied = copySources(sources);
377
+ generateIndex(copied, sel);
378
+ writeManifest(copied, sel);
379
+
380
+ console.log(`\n✓ Copied ${copied.length} standard files to coding-standards/`);
381
+ console.log(`✓ Generated coding-standards/INDEX.md (selection: ${describeSelection(sel)})`);
382
+ console.log("\nNext: point your AI agent at coding-standards/INDEX.md (e.g. from AGENTS.md).");
34
383
  }
35
- console.log("\nDone!");
384
+
385
+ main().catch((err) => {
386
+ console.error("\n✗ Installation failed:", err.message);
387
+ process.exit(1);
388
+ });
package/package.json CHANGED
@@ -1,19 +1,29 @@
1
1
  {
2
2
  "name": "@jeffrey2423/coding-standards",
3
- "version": "1.0.0",
4
- "description": "Copy coding standards and architecture guides into your project",
3
+ "version": "2.0.0",
4
+ "description": "Pick and copy modern, AI-ready coding standards and architecture guides into your project",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "coding-standards": "bin/cli.js"
8
8
  },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
9
12
  "files": [
10
13
  "bin/",
11
14
  "standards/"
12
15
  ],
16
+ "dependencies": {
17
+ "@clack/prompts": "^0.7.0"
18
+ },
13
19
  "keywords": [
14
20
  "coding-standards",
15
21
  "architecture",
16
22
  "clean-architecture",
17
- "ddd"
23
+ "ddd",
24
+ "microservices",
25
+ "microfrontends",
26
+ "ai-assisted",
27
+ "agents"
18
28
  ]
19
29
  }
@@ -0,0 +1,112 @@
1
+ ---
2
+ title: Event-Driven Communication
3
+ platform: backend
4
+ track: distributed-architecture
5
+ load_when: "Coordinating microservices — outbox, idempotency, sagas, and correlation."
6
+ updated: 2026-06
7
+ ---
8
+
9
+ # Event-Driven Communication
10
+
11
+ Microservices communicate by **asynchronous events** through a broker. Synchronous HTTP is reserved for when the caller needs the answer now.
12
+
13
+ > **The rule:** async when you can, sync when it hurts not to. Most microservice pain comes from abusing synchronous HTTP chains.
14
+
15
+ ## Why async by default
16
+
17
+ | Sync HTTP chain `A→B→C→D` | Events via broker |
18
+ |---|---|
19
+ | Temporal coupling — D down ⇒ A fails | B/C/D can be down without affecting A |
20
+ | Availability = product of each link | Broker absorbs spikes + retries |
21
+ | Latency = sum of the chain | Consumers process at their own pace |
22
+
23
+ **Use sync** only for "need it now" reads: validate stock before closing a sale, authenticate, check entitlement. If unsure, **start async** — converting async→sync later is easier than untangling broken chains.
24
+
25
+ ## Transactional Outbox (mandatory)
26
+
27
+ Persisting the aggregate and publishing the event must be **atomic**. The integration event is written to an `outbox_messages` table **in the same transaction** as the aggregate change; a background worker publishes it to the broker.
28
+
29
+ ```sql
30
+ BEGIN;
31
+ UPDATE orders SET status = 'confirmed' WHERE id = '…';
32
+ INSERT INTO outbox_messages (id, message_type, body, occurred_at)
33
+ VALUES ('…', 'OrderConfirmedV1', '{…}', now());
34
+ COMMIT;
35
+ ```
36
+
37
+ Guarantees:
38
+ - DB fails before commit → nothing persisted, nothing published.
39
+ - Broker fails after commit → event stays in outbox, retried.
40
+ - **Never** any drift between persisted aggregate and published event. Eliminates the need for distributed transactions (2PC).
41
+
42
+ **Implementation:** use **Wolverine** (MIT) — it provides the Outbox natively. Avoid the now-commercial MediatR/MassTransit per the [open-source-only policy](../technology-stack.md). The pattern is what matters.
43
+
44
+ ## Idempotency (mandatory)
45
+
46
+ The outbox + broker give **at-least-once** delivery, so the same event can arrive twice. Every consumer **MUST** be idempotent.
47
+
48
+ ```sql
49
+ CREATE TABLE processed_messages (
50
+ message_id UUID PRIMARY KEY,
51
+ event_type VARCHAR(100),
52
+ tenant_id UUID,
53
+ processed_at TIMESTAMPTZ
54
+ );
55
+ ```
56
+
57
+ The consumer inserts `message_id` into `processed_messages` **in the same transaction** as the domain change; if `message_id` already exists, it skips. (Alternative for high volume: dedupe on a natural key — "`OrderClosedV1` for `order_id=X` processes once".)
58
+
59
+ ## Sagas / process managers
60
+
61
+ A business process spanning multiple contexts is a **saga**, not an HTTP chain. The saga reacts to integration events, emits commands, persists its state, and uses **compensations** (not distributed rollback) on failure.
62
+
63
+ ```
64
+ on OrderConfirmedV1 → ReserveStockCommand (state: AWAITING_STOCK)
65
+ on StockReservedV1 → IssueInvoiceCommand (state: AWAITING_INVOICE)
66
+ on InvoiceIssuedV1 → … COMPLETED
67
+ on StockInsufficientV1→ CancelOrderCommand (compensation)
68
+ on InvoiceFailedV1 → ReleaseStockCommand + CancelOrderCommand
69
+ ```
70
+
71
+ A compensation is a **new business fact** ("order cancelled"), not a DB undo — the events happened and consumers reacted.
72
+
73
+ ## Correlation ID
74
+
75
+ Every message and request carries a `CorrelationId` propagated end-to-end. With **OpenTelemetry**, the `traceparent` header does this automatically, giving a distributed trace across async hops. Without it, debugging async flows is impossible.
76
+
77
+ ## Integration event shape
78
+
79
+ ```json
80
+ {
81
+ "eventId": "evt_abc123",
82
+ "eventType": "OrderConfirmedV1",
83
+ "tenantId": "tenant_042",
84
+ "occurredAt": "2026-05-15T14:32:11Z",
85
+ "correlationId": "corr_xyz",
86
+ "data": { "orderId": "ord_001", "total": 125000, "currency": "COP" }
87
+ }
88
+ ```
89
+
90
+ Required: `eventId` (idempotency), `eventType` (versioned), `tenantId`, `occurredAt` (when it happened, not when published), `correlationId`, `data`.
91
+
92
+ > Consider the **CloudEvents 1.0** envelope (`id`/`source`/`type`/`specversion`) for cross-system event formatting; it's the CNCF standard and complements broker-internal events.
93
+
94
+ ## Conventions
95
+
96
+ - **MUST** name events in the past tense with an explicit version: `OrderConfirmedV1`. Events are facts, not commands.
97
+ - **MUST** include `tenantId` in every event.
98
+ - **MUST** freeze a version's contract once published; breaking change ⇒ new version; both coexist during deprecation.
99
+ - **SHOULD** publish events for **business-significant state transitions**, not every property change. If unsure, don't publish — adding events later is easier than retiring them.
100
+ - **SHOULD NOT** dump the internal aggregate state into an event — design the event for its consumers.
101
+
102
+ ## Sync calls (when necessary)
103
+
104
+ When you must call synchronously, call the other service's **public API** (never its DB). Apply: retries with exponential backoff, short timeout (≤ 5s), **circuit breaker**, propagate `X-Correlation-Id`, `Idempotency-Key` on idempotent POSTs. Use **Microsoft.Extensions.Resilience (Polly v8)**.
105
+
106
+ ## Broker is internal
107
+
108
+ The broker (RabbitMQ/Kafka) is for **internal** service-to-service traffic only. External integrators receive **webhooks**, not broker access — see [`public-api-facade.md`](public-api-facade.md).
109
+
110
+ ## Anti-patterns
111
+
112
+ - Long sync HTTP chains; events disguised as commands (`PleaseReserveStockEvent`); events without `tenantId`; non-idempotent consumers; per-property events; events leaking internal models; missing correlation ID.
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: Microservice Anatomy
3
+ platform: backend
4
+ track: distributed-architecture
5
+ load_when: "Designing or implementing a microservice — its project layout, layers, and event model."
6
+ updated: 2026-06
7
+ ---
8
+
9
+ # Microservice Anatomy
10
+
11
+ Every microservice follows the **same** internal structure. Structural consistency lets the team move between services without relearning conventions, and lets operations use one toolset for all. Implements [`core/clean-architecture-ddd.md`](../../core/clean-architecture-ddd.md).
12
+
13
+ ## The decisions
14
+
15
+ - **DB-per-service (real).** Each bounded context owns its database. **No other service touches it** — access is via API or events only. A shared DB is a distributed monolith.
16
+ - **Clean Architecture + DDD inside** each service (four layers, strict inward dependency rule).
17
+ - **Async by default** between services; sync only when the caller needs the answer now (see [`event-driven.md`](event-driven.md)).
18
+ - **Public, versioned contracts** at the edge (see [`public-api-facade.md`](public-api-facade.md)).
19
+
20
+ ## Standard solution layout (5 projects)
21
+
22
+ ```
23
+ src/
24
+ ├── {Context}.Domain/ # aggregates, value objects, domain events — zero framework refs
25
+ ├── {Context}.Application/ # use cases (commands/queries), DTOs, port interfaces
26
+ ├── {Context}.Infrastructure/ # EF Core, repositories, outbox, consumers, ACL
27
+ ├── {Context}.Api/ # Minimal API endpoints
28
+ └── {Context}.IntegrationEvents/ # published language — distributable package (NuGet)
29
+ ├── V1/
30
+ └── V2/
31
+ ```
32
+
33
+ `IntegrationEvents` is the **only** project that crosses the context boundary; publish it as a versioned package so other services/adapters consume it.
34
+
35
+ ### Dependency rule
36
+
37
+ - `Domain` → depends on nothing.
38
+ - `Application` → Domain.
39
+ - `Infrastructure` → Application + Domain (implements Application's interfaces).
40
+ - `Api` → Application.
41
+ - `IntegrationEvents` → primitives only.
42
+
43
+ > Swapping the DB engine, broker, or HTTP framework must touch **only Infrastructure**. The domain never finds out.
44
+
45
+ ## Layer rules
46
+
47
+ **Presentation (Api).** Validates request format, authenticates (JWT + tenant claim), maps HTTP → Command/Query, returns the serialized result. **No business logic, no DB access, no calls to other services.**
48
+ - Path: `/api/v{version}/{context}/{resource}`; standard HTTP verbs/status; errors as Problem Details (RFC 9457).
49
+ - Reads headers: `Authorization: Bearer`, `Idempotency-Key`, `X-Correlation-Id`.
50
+
51
+ **Application.** CQRS use cases that **orchestrate**, never decide. A command handler: load aggregate → invoke its behavior → persist via Unit of Work → let domain events dispatch. Declares ports (`I{Aggregate}Repository`, `IUnitOfWork`, `IIntegrationEventOutbox`, `ITenantContext`). Holds the handler that **translates domain events → integration events**.
52
+
53
+ **Domain.** Pure language. Aggregates guard invariants and expose behavior (`Confirm()`, not setters); emit domain events; reference other aggregates by ID. Value objects validate on construction. Every invariant has a unit test including the violation case.
54
+
55
+ **Infrastructure.** Repository impls (one per aggregate root, load the whole aggregate, no `IQueryable` leaking out), ORM mappings, the **transactional outbox**, inbound **consumers** (idempotent), and the **Anticorruption Layer** that translates other contexts' models.
56
+
57
+ ## Mediation / messaging (2026)
58
+
59
+ Per the [open-source-only policy](../technology-stack.md), use **Wolverine** (MIT) — it provides command/query mediation, the message bus, and the transactional Outbox in one package. Do **not** use the now-commercial MediatR/MassTransit. The `Mediator` source-generator or hand-rolled dispatch are also fine. The **pattern** (CQRS, domain-vs-integration events, outbox) matters, not the library.
60
+
61
+ ## Domain events vs integration events
62
+
63
+ | Aspect | Domain Event | Integration Event |
64
+ |---|---|---|
65
+ | Location | Domain layer | `IntegrationEvents` package |
66
+ | Audience | Inside the service | Other services + third parties |
67
+ | Language | Ubiquitous, internal | Public, versioned |
68
+ | Persistence | In memory | Outbox + broker |
69
+ | Versioning | none | strict (`V1`, `V2`, backward-compatible) |
70
+ | Example | `OrderConfirmedDomainEvent` | `OrderConfirmedV1` |
71
+
72
+ Explicit translation between them protects the domain: the internal model can evolve freely without breaking external consumers.
73
+
74
+ ## Request flow
75
+
76
+ ```
77
+ HTTP → JWT/tenant extracted → endpoint maps to Command → handler loads aggregate
78
+ → aggregate applies rules + emits domain event → persist (aggregate + outbox row, one tx)
79
+ → commit → domain event dispatched in-memory → translated to IntegrationEvent V1
80
+ → (background) outbox worker publishes to broker → other services consume idempotently
81
+ ```
82
+
83
+ ## Naming
84
+
85
+ | Thing | Convention | Example |
86
+ |---|---|---|
87
+ | Bounded context | singular | `Catalog`, `Sales` |
88
+ | Aggregate | singular | `Order`, `Product` |
89
+ | Value object | descriptive | `Money`, `Sku` |
90
+ | Domain event | past + `DomainEvent` | `OrderConfirmedDomainEvent` |
91
+ | Integration event | past + version | `OrderConfirmedV1` |
92
+ | Command | imperative + `Command` | `ConfirmOrderCommand` |
93
+ | Query | `Get…Query` | `GetOrderByIdQuery` |
94
+
95
+ ## Vertical slices
96
+
97
+ Clean Architecture layering and **Vertical Slice** organization combine well: keep the layer boundaries, but organize Application code by feature/use-case slice (command + handler + validator + DTO together) rather than by technical folder. This keeps related code cohesive and is the common 2026 default for new services.
98
+
99
+ ## Anti-patterns
100
+
101
+ - Anemic CRUD-only service (no domain behavior) → it's a table with an API, not a microservice.
102
+ - Joins across service DBs → breaks DB-per-service; the context boundaries are probably wrong.
103
+ - Business rules in validators/endpoints → rules live in the domain.
104
+ - Long synchronous HTTP chains `A→B→C→D` → any link down tumbles the chain; go async.
105
+ - Shipping business code inside the `IntegrationEvents` package → it must contain only contracts.
106
+ - Forgetting idempotency in consumers → at-least-once delivery duplicates effects.