Architecture Decisions#

Every software system is shaped by hundreds of decisions — technology choices, structural patterns, integration approaches. The best teams make these decisions explicitly, traceably, and reversibly. Architecture Decision Records (ADRs) are the primary tool for this discipline.


Architecture Decision Records (ADRs)#

An ADR documents one significant architectural decision along with its context, rationale, and consequences.

Standard Format#

# ADR-001: Use PostgreSQL for order storage

## Status
Accepted (2024-03-15)

## Context
The orders service needs persistent storage supporting:
- ACID transactions for financial data
- Complex queries for reporting
- High write throughput (10K orders/hour peak)

We considered PostgreSQL, DynamoDB, and MongoDB.

## Decision
We will use PostgreSQL (via AWS RDS).

## Consequences
### Positive
- Strong consistency guarantees
- Rich query capabilities (JOIN, aggregates)
- Team expertise exists

### Negative
- Vertical scaling limits (vs DynamoDB)
- Operational overhead (backups, failover)
- Schema migrations required for changes

### Risks
- May need read replicas at 100K orders/hour

ADR Lifecycle#

StatusMeaning
ProposedUnder discussion, not yet approved
AcceptedApproved and active
DeprecatedNo longer recommended but existing code may use it
SupersededReplaced by another ADR (link to successor)

Where to Store ADRs#

LocationProsCons
Repository (docs/adr/)Versioned with code, close to contextScattered across repos
Wiki/ConfluenceSearchable, cross-project visibilityNot versioned with code
Dedicated ADR repoCentral registry, easy to browseDisconnected from implementation

Recommendation: Store in the repo for service-specific decisions; use a central wiki for cross-cutting decisions.


Decision Criteria#

Not all decisions deserve equal rigor. Evaluate decisions on three axes:

Reversibility#

CategoryExamplesProcess
One-way door (irreversible)Database choice, public API contract, cloud providerFull ADR, team review, stakeholder alignment
Two-way door (reversible)Library choice, internal data format, CI toolLightweight ADR, team discussion
TrivialVariable naming, folder structureJust decide, no ADR needed

Blast Radius#

ScopeImpactGovernance
Single serviceOnly one team affectedTeam decides autonomously
Multiple servicesCross-team coordination neededArchitecture review + ADR
Organization-wideStandards, platforms, securityArchitecture board approval

Alignment#

Decisions should align with:

  • Business strategy — Does this support where the company is going?
  • Technical strategy — Is this consistent with the tech radar?
  • Team capabilities — Can the team build and maintain this?
  • Existing decisions — Does this contradict prior ADRs?

Fitness Functions#

Fitness functions are automated checks that validate whether the system still meets its architectural goals over time.

Types#

TypeValidatesExample
AtomicSingle characteristicP95 latency < 200ms
HolisticCombined characteristicsAvailability AND cost within budget
TriggeredOn event (deploy, PR)No circular dependencies in build
ContinuousAlways runningSLO dashboards, chaos tests

Examples#

# Deployment fitness function
- name: "No service takes > 15 minutes to deploy"
  metric: deployment_duration_p95
  threshold: "< 900s"
  frequency: per_deployment

# Coupling fitness function  
- name: "No service has > 3 synchronous dependencies"
  check: dependency_graph_analysis
  threshold: "max_sync_deps <= 3"
  frequency: weekly

# Security fitness function
- name: "No critical CVEs in production"
  check: vulnerability_scan
  threshold: "critical_count == 0"
  frequency: daily

Architecture Tests (Code-Level)#

// ArchUnit (Java) — enforce layering
@ArchTest
static final ArchRule layerRule = layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Repository").definedBy("..repository..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
# Python — enforce no circular imports
# deptry / import-linter
[importlinter]
root_package = myapp
[importlinter:contract:layers]
name = Layered architecture
type = layers
layers =
    myapp.api
    myapp.service
    myapp.repository

Evolutionary Architecture#

Architecture should evolve incrementally rather than be designed upfront for all future needs.

Principles#

PrincipleMeaning
Last responsible momentDefer decisions until you have the most information
Small, reversible stepsPrefer many small changes over large irreversible ones
Fitness functionsAutomate validation of architectural properties
Sacrificial architectureAccept that today’s design will be replaced

Guided Evolution#

              ┌─────────────────────────────────────────┐
              │  Fitness Functions (automated guardrails) │
              └─────────────────┬───────────────────────┘
                                │
    Start ──→ [Simple] ──→ [Modular] ──→ [Distributed] ──→ [Adaptive]
                │               │              │                │
            Monolith      Modular mono    Microservices    Event-driven

Avoiding Evolutionary Traps#

TrapConsequencePrevention
Premature abstractionComplexity without valueWait for the third use case
Accidental couplingCan’t evolve independentlyFitness functions for coupling
Missing testsCan’t refactor safelyTest coverage as fitness function
Knowledge silosDecisions lost when people leaveADRs + architecture reviews

Tech Radar#

A tech radar communicates the organization’s technology strategy:

RingMeaningAction
AdoptProven, recommended for broad useDefault choice for new projects
TrialWorth exploring in low-risk projectsUse with awareness of immaturity
AssessInteresting, worth investigatingResearch and prototype only
HoldDo not start new work with thisMigrate away when practical

Example Entries#

TechnologyRingRationale
PostgreSQLAdoptProven, team expertise, managed service available
io_uringTrialPerformance gains demonstrated; limited production experience
CockroachDBAssessInteresting for multi-region; evaluate operational costs
MongoDB (unstructured data)HoldPast issues with data consistency; prefer PostgreSQL JSONB

Maintaining the Radar#

  • Review quarterly with engineering leads
  • Any engineer can propose additions/movements
  • Decisions to move items require lightweight ADR
  • Publish openly — transparency drives adoption

Architecture Reviews#

When to Review#

TriggerReview Type
New service or major featureDesign review (before implementation)
ADR with org-wide blast radiusArchitecture board review
Post-incident (architectural root cause)Post-mortem architecture review
QuarterlyHealth check / fitness function review

Lightweight Architecture Review#

## Review Checklist

### Structural
- [ ] Clear bounded context / responsibility
- [ ] Dependencies explicit and minimal
- [ ] No circular dependencies
- [ ] Data ownership clear

### Quality Attributes
- [ ] Scalability approach documented
- [ ] Failure modes identified
- [ ] Performance targets defined
- [ ] Security model documented

### Evolutionary
- [ ] Decision reversibility assessed
- [ ] ADR written for significant decisions
- [ ] Fitness functions defined
- [ ] Migration path from current state clear

Communicating Decisions#

Decisions are worthless if the team doesn’t know about them.

Communication Channels#

AudienceChannelFormat
Implementing teamADR in repo + team meetingTechnical detail
Adjacent teamsArchitecture guild + shared wikiContext + impact
LeadershipArchitecture review deckBusiness value + risks
New joinersOnboarding docs referencing ADRsNarrative + links

Decision Announcement Template#

## 📐 Architecture Decision: [Title]

**What:** [One-line summary of the decision]
**Why:** [Business/technical driver in plain language]
**Impact:** [Who needs to change behavior and how]
**ADR:** [Link to full ADR]
**Questions:** [Contact person or channel]

Common Mistakes#

MistakeConsequenceFix
Decision by committeeSlow, watered-down choicesClear decision maker (RACI)
Not documenting contextFuture team reverses good decisionsAlways write the “why”
Documenting after the factMissing rationale and alternativesWrite ADR during discussion
Over-governingTeams slow to shipOnly govern high-blast-radius decisions

Key Takeaways#

  1. Write ADRs for any decision that is hard to reverse or affects multiple teams.
  2. Classify decisions by reversibility and blast radius — govern accordingly.
  3. Fitness functions automate architectural compliance — don’t rely on manual reviews alone.
  4. Architecture should evolve incrementally — avoid big upfront design.
  5. A tech radar communicates strategy; keep it alive with quarterly reviews.
  6. Communicate decisions through multiple channels — an ADR nobody reads is worthless.
  7. The goal is not perfect architecture but informed, traceable, reversible decisions.