Solution Architecture · Lesson 3 of 6
Architecture Decision Records — Documenting Trade-offs
Why ADRs Exist
Code tells you what the system does.
Comments (sometimes) tell you how.
Nothing tells you why — unless you write it down.
Without ADRs:
→ New engineer asks: "Why are we using a modular monolith instead of microservices?"
→ Answer: "I don't know, Dave left two years ago."
→ Engineer refactors toward microservices without understanding the constraint that ruled them out.
→ Six months later: same constraints resurface and the work is rolled back.
With ADRs:
→ ADR-004: "We chose a modular monolith because the team has no Kubernetes operational experience
and the hospital's SLA does not require per-service independent scaling."
→ Engineer reads the ADR, understands the constraint is still valid, proposes a targeted change
instead of a full rewrite.
ADRs are insurance against institutional amnesia.ADR Format
# ADR-007: Use Schema-per-Module for Data Isolation
**Status:** Accepted
**Date:** 2026-03-14
**Context:**
The Clinical Platform is built as a modular monolith with five bounded contexts:
Patients, Prescriptions, LabResults, Billing, and Notifications.
All modules currently share a single SQL Server database with no schema boundaries.
Developers have begun accidentally writing LINQ queries that JOIN across module tables,
and a recent incident caused prescription data to be corrupted when the Patients
team ran a migration that also modified prescription tables (shared DbContext).
**Decision:**
Each module will own a dedicated SQL schema (patients, prescriptions, lab_results, billing).
Each module will have its own EF Core DbContext mapping only its schema.
Cross-schema JOIN queries in application code are prohibited.
Migrations are managed per module with separate output directories.
**Options Considered:**
1. Separate databases per module — stronger isolation, but adds operational complexity
(connection strings, backup coordination) the team cannot manage with current tooling.
2. Schema-per-module in one database — enforces logical isolation with shared operations.
Chosen option.
3. No schema separation — status quo, known to cause accidents. Rejected.
**Consequences:**
+ Accidental cross-module queries fail at compile time (wrong DbContext).
+ Migrations are isolated — Patients team can release without affecting Prescriptions.
+ Cross-module queries require explicit API calls or event subscriptions.
- Cross-schema reporting queries must be done via a dedicated reporting database
or a read model, not via application-layer joins.
- All existing tables must be migrated to their new schemas (one-time migration cost).
**Review Date:** 2026-09-14 (after 6 months in production)ADR File Organization
docs/
architecture/
decisions/
ADR-001-modular-monolith-over-microservices.md
ADR-002-ef-core-as-orm.md
ADR-003-mediatR-for-cqrs.md
ADR-004-fluentvalidation-for-input-validation.md
ADR-005-serilog-with-seq.md
ADR-006-azure-app-service-deployment.md
ADR-007-schema-per-module-data-isolation.md
ADR-008-outbox-pattern-for-module-events.md
README.md → index of all ADRs with status<!-- docs/architecture/decisions/README.md -->
# Architecture Decision Records
| ADR | Title | Status | Date |
|------|----------------------------------------|------------|------------|
| 001 | Modular monolith over microservices | Accepted | 2026-01-10 |
| 002 | EF Core as primary ORM | Accepted | 2026-01-10 |
| 003 | MediatR for CQRS | Accepted | 2026-01-15 |
| 004 | FluentValidation for input validation | Accepted | 2026-01-15 |
| 005 | Serilog with Seq for structured logging| Accepted | 2026-01-20 |
| 006 | Azure App Service deployment | Accepted | 2026-02-01 |
| 007 | Schema-per-module data isolation | Accepted | 2026-03-14 |
| 008 | Outbox pattern for module events | Proposed | 2026-04-02 |ADR Statuses
Proposed → Written, not yet reviewed/accepted by the team
Accepted → Team decision, being implemented
Deprecated → No longer applies (replaced by a newer ADR)
Superseded → Replaced by ADR-NNN (link to the new ADR)
Use "Superseded by ADR-012" rather than deleting ADR-007.
Deleting history is how institutional amnesia starts.
Bad practice: marking old ADRs as "Rejected" retroactively when the decision was actually implemented.
An ADR documents what WAS decided, not what you wish had been decided.When to Write an ADR
Write an ADR when:
→ You are choosing between two or more non-trivial options
→ The decision will be hard or expensive to reverse
→ The decision will surprise or confuse a future team member
→ You are deliberately deviating from a team standard or industry default
→ The decision has regulatory or compliance implications
Do NOT write an ADR for:
→ Obvious choices ("we'll use HTTPS")
→ Coding style or formatting (put in .editorconfig or a style guide)
→ Decisions that affect one file and can be reversed in 10 minutes
Examples for a clinical .NET system:
Write ADR: choosing modular monolith vs microservices
Write ADR: using soft deletes for all clinical data
Write ADR: FHIR R4 vs proprietary patient data format
Write ADR: outbox pattern vs synchronous event publishing
Skip ADR: choosing FluentAssertions over built-in assertions in tests
Skip ADR: naming convention for local variablesTooling Options
Option 1: Markdown files in the repo (recommended)
→ ADRs live alongside code in version control
→ PRs can include ADRs alongside feature changes
→ GitHub/GitLab render Markdown natively
→ Use adr-tools CLI: `adr new "Schema per module data isolation"`
Option 2: Confluence or Notion
→ Easier for non-developers to read
→ Harder to link to specific code versions
→ Risk of diverging from the codebase (nobody keeps docs in sync)
Option 3: Comments in code
→ Only appropriate for narrow, local decisions
→ Not suitable for system-level architectural decisions
Recommendation: Markdown files in /docs/architecture/decisions/
committed to the same repo as the code they describe.Production issue I've seen: A .NET system I joined had been running for 4 years with no ADRs. The codebase had three different HTTP client patterns: raw HttpClient in some services, IHttpClientFactory in others, and Refit in a third set. Nobody knew which was the "right" one or why. Each pattern had been chosen by a different developer in a different month. When we tried to standardize, we discovered one pattern had been used because of a specific TLS certificate issue with the hospital proxy — removing it broke prod. An ADR written in week 1 of that decision ("we use raw HttpClient here because IHttpClientFactory doesn't support client certificates with this proxy configuration") would have saved 3 days of investigation.
Key Takeaway
An ADR documents Context (why the decision was needed), Decision (what was chosen), Options Considered (what was rejected and why), and Consequences (what improves and what gets harder). Store ADRs as Markdown files in version control alongside the code. Write an ADR for any decision that is hard to reverse, will surprise future engineers, or has compliance implications. Never delete old ADRs — mark them Superseded and link to the replacement.