Architecture Decision Records
This directory contains Architecture Decision Records (ADRs) for the Protean framework.
What are ADRs?
An Architecture Decision Record captures a single architectural decision along with its context and consequences. ADRs are short documents that record why we made a particular choice at a particular time. Future contributors read them to learn why the system works the way it does, in addition to what it does.
The practice was introduced by Michael Nygard in his blog post Documenting Architecture Decisions.
Why Protean uses ADRs
Protean is an opinionated DDD framework with a growing IR-based "domain compiler" architecture. Many of our decisions involve trade-offs between DDD purity, developer ergonomics, and tooling extensibility. We record these decisions so that contributors do not reopen questions we have already settled. New contributors read the records to learn the reasons behind the framework's design.
Naming convention
ADRs are numbered sequentially with four-digit zero-padded prefixes and kebab-case titles:
NNNN-short-kebab-title.md
Examples:
0001-monotonic-integer-event-versioning.md0003-one-ir-document-per-bounded-context.md
ADR-0000 is reserved for the guiding principles document. It is not a decision record itself but a meta-document that captures the principles governing all Protean architectural decisions.
Status lifecycle
Each ADR has one of the following statuses:
- Proposed: Under discussion, not yet committed to.
- Accepted: The decision has been made and is in effect. An Accepted ADR has no open questions.
- Deprecated: The decision is no longer relevant (technology changed, feature removed).
- Superseded by ADR-XXXX: Replaced by a newer decision.
Status changes are recorded by editing the ADR's Status field. The original ADR is never deleted; it remains as historical context.
An ADR states each decision with its reason
An ADR follows TEMPLATE.md.
- It records one decision, or a few decisions that stand or fall together. A decision that someone could reopen on its own gets its own ADR.
- The title and every decision heading are full sentences that state the
decision, such as "Provider
sqlserves SQLite and PostgreSQL". - Each decision is a short statement or a few bullets, followed by one or two sentences of reason. A reader who missed the discussion follows it on one read. Terms the reader already knows get no explanation.
- Context states the problem and the facts that shaped the decision. The state before the decision reads "Before this decision, ...".
- Consequences has two lists, Positive: and Negative:. The Negative list names what the decision costs.
- Each alternative is a list item: the option in bold as a sentence, then "Rejected:" and the reason.
- A dependency names the capability the decision needs, under an optional Dependencies heading.
An ADR stays true for as long as it stands
When a decision stops being true, a new ADR supersedes it. Until then, every sentence in it holds. So an ADR leaves out:
- Time words: "today", "currently", "already", "for now", "for this release", "once X lands", "later". A future change is written as a rule that always holds, such as "any replacement must pass the same conformance suite".
- Open questions and open work. Questions go to issues, and an ADR is Accepted only after they are answered.
- Plans: which issue or pull request does what, in what order, or what holds during a migration. Plans belong in issues.
- Issues and epics as the source of a decision. The ADR itself states the decision and its reason. An issue link can serve as evidence, such as a bug report that shows the problem.
- Lists of names: method signatures, record fields, module lists and file trees. Names belong in docstrings and reference pages. A name appears in an ADR when the decision is about that name.
- Counts of code facts, such as the number of methods on a class. The next change makes them wrong.
- Sentences about the ADR itself, such as "The tables below list...".
Referencing ADRs
In code comments:
# See ADR-0001 for why we use monotonic integers, not semver
In commit messages:
Implement event upcaster chain (ADR-0001)
In other ADRs, reference by number: "as established in ADR-0003."
Template
See TEMPLATE.md in the adr/ directory for the ADR template.
Current ADRs
| # | Title |
|---|---|
| 0000 | Guiding Principles |
| 0001 | Monotonic Integer Event Versioning |
| 0002 | Event Publication Visibility |
| 0003 | One IR Document per Bounded Context |
| 0004 | Release Workflow and Breaking Change Policy |
| 0005 | IR-First Schema Generation |
| 0006 | Standard JSON Schema with X-Extensions |
| 0007 | Domain-Scoped OpenTelemetry Providers |
| 0008 | Centralized Telemetry Gateway and TraceParent Bridging |
| 0009 | Concurrent Event Processing Strategy |
| 0010 | Logging Overhaul and Wide Event Architecture |
| 0011 | Engine Shutdown and Resource Lifecycle Contract |
| 0012 | Health Check Architecture |
| 0013 | Optimistic Concurrency and Claim Contract |
| 0014 | Aggregate Metadata via Decorator Parameters |
| 0015 | Event-Store Append as the Durable Anchor in the Commit Sequence |
| 0016 | Breadth-First Synchronous Event Dispatch |
| 0017 | Consume-Side Idempotency for Projectors |
| 0018 | Type-safety adoption strategy |
| 0019 | Check-engine determinism boundary and a behavioral analysis substrate |
| 0020 | Dependency Floor Policy |
| 0021 | A uuid identity is a string in Python |
| 0022 | Pre-Persist Aggregate Enricher for Lifecycle/Audit Fields |
| 0023 | Query API Conformance Contract and Capability Tiers |
| 0024 | Event Store Read Position Contract |
| 0025 | Gap-Safe Checkpointing for $all Subscriptions |
| 0026 | Sanitized String Values Satisfy Their Field's Length Bounds |
| 0027 | The Unit of Work is a Real Database Transaction |
| 0028 | Partition-Per-Key Sequential Processing (sequential_by) |
| 0029 | Runtime Dependency Boundary and Feature Extras |
| 0030 | Canonical Generated Project Layout |
| 0031 | A Handler Method Persists or Calls Out, Never Both |
| 0032 | Annotation File for the Event Model Renderer |
| 0033 | ChangePlan is a Versioned Structural Contract |
| 0034 | Derived Project Manifest |
| 0035 | Generation-Gap Seam Between Generated Wiring and Hand-Owned Logic |
| 0036 | Configurable Graceful Drain Window |
| 0037 | Idempotent Managed Files and the dx State File |
| 0038 | Canonical field-declaration style for mypy-strict domains |
| 0039 | Ship the developer-experience pack as package data |
| 0040 | The Schema-Evolution Ladder |
| 0041 | Textual Event-Model Grammar over the IR |
| 0042 | The Subscription Owns Retry and Dead-Lettering |
| 0043 | Typed query dispatch through a generic query base |
| 0044 | The custom-field extension point is a public Custom factory |
| 0045 | Handle only the errors you expect |
| 0046 | One aggregate instance per Unit of Work |
| 0047 | The mypy plugin makes a decorated element a subclass of its base |
| 0048 | Protean stores events in SQLite and PostgreSQL through SQLAlchemy |
| 0049 | An adapter port is a Protocol of adapter operations, wrapped by a framework service |