Skip to content

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.md
  • 0003-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 sql serves 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