Designing the System

Architecture Decision Records

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

আগের lesson-এ আমরা আমাদের প্রথম RFC লিখেছি।

RFC-এ আমরা capture করেছি:

  • system context
  • goals
  • non-goals
  • proposed architecture
  • domain model
  • major workflows
  • transaction requirements
  • payment boundary
  • risks
  • alternatives
  • open questions

RFC একটি broad technical proposal।

কিন্তু software project evolve করার সঙ্গে সঙ্গে কিছু specific architectural decision দীর্ঘ সময় relevant থাকে।

For example:

Why did we choose a modular monolith?

Why is Inventory separate from Product?

Why does Order Item store purchase-time price?

Why is payment provider logic isolated behind a boundary?

ছয় মাস পরে একজন নতুন engineer code দেখে decision দেখতে পারে।

কিন্তু সে decision-এর reason নাও জানতে পারে।

এই problem solve করার একটি lightweight mechanism হলো:

Architecture Decision Record

সংক্ষেপে:

ADR

What Is an ADR?

ADR হলো একটি ছোট document যেখানে একটি গুরুত্বপূর্ণ architectural decision এবং সেই decision-এর context record করা হয়।

Conceptually:

Context
    ↓
Decision
    ↓
Consequences

একটি ADR-এর মূল প্রশ্ন:

We made this decision. Why did we make it, and what does it imply?

ADR সাধারণত implementation specification নয়।

এটি decision history।


Why Record Decisions?

Code usually tells us:

what exists

কিন্তু সবসময় বলে না:

why it exists

Suppose repository structure:

product/
inventory/
order/
payment/

দেখে future engineer বলতে পারে:

Why didn't we make Product, Inventory, and Order separate services?

Code answer দেবে না।

কিন্তু ADR বলতে পারে:

Current workflows require local transactional consistency.
The current scale and requirements do not justify
distributed-system complexity.
Therefore we chose a modular monolith.

এখন decision understandable।


The "Why" Often Disappears First

Implementation থেকে আমরা দেখতে পারি:

orderItem.setUnitPrice(product.getPrice());

কিন্তু কয়েক মাস পরে কেউ বলতে পারে:

We already have Product.price. Why store price on OrderItem too?

তারপর সে "duplication cleanup" করে historical price remove করে দিতে পারে।

যদি ADR explain করে:

Order Item stores purchase-time pricing
because historical orders must not change
when Product price changes.

তাহলে সেই context preserved থাকে।


ADR Is Not Documentation of Every Choice

আমরা ADR লিখব না:

Use constructor injection.
Name this class OrderController.
Use package `order.api`.
Use a private helper method.

এসব সাধারণ implementation choices।

ADR দরকার এমন decision-এর জন্য যা:

architecturally meaningful

difficult or costly to reverse

affects multiple parts of the system

has important trade-offs

may be questioned later

RFC vs ADR

একটি simple distinction:

RFC

What design are we proposing for this larger problem?

ADR

What important decision did we make,
and why?

Our RFC:

RFC-001
Initial Order Management Backend Architecture

এর মধ্যে অনেক decision আছে।

তার মধ্যে কিছু independent ADR হিসেবে preserve করা useful।


ADR Is Usually Short

একটি useful ADR 10 pages হতে হবে না।

Typical structure:

# ADR-XXX: Decision Title

## Status

## Context

## Decision

## Alternatives Considered

## Consequences

কখনো additional sections থাকতে পারে:

Risks

Notes

References

কিন্তু simplicity valuable।


ADR Status

Common statuses:

Proposed

Accepted

Superseded

Deprecated

For our course project, simple lifecycle enough।

Example:

Status: Accepted

Later যদি decision replace হয়:

Status: Superseded by ADR-007

এতে architecture history disappear করে না।


Never Rewrite History Silently

Suppose ADR-001 says:

Use a modular monolith.

দুই বছর পরে system microservices-এ split হলো।

Old ADR delete করে:

We use microservices.

লিখে দিলে historical reasoning হারিয়ে যায়।

Better:

ADR-001
Status: Superseded

and:

ADR-010
Split Order and Inventory into independent services.

এখন history বোঝা যায়:

What was true then?

What changed?

Why did architecture evolve?

Where Will ADRs Live?

Our repository structure:

docs/
├── rfcs/
├── adr/
└── incidents/

Example:

docs/adr/001-use-modular-monolith.md
docs/adr/002-separate-inventory-from-product.md
docs/adr/003-preserve-order-item-purchase-price.md

Numbering gives stable references।


Which Decisions Deserve ADRs Now?

From our design work, several candidate decisions exist।

We should not turn every RFC paragraph into an ADR।

The most durable current decisions are:

ADR-001
Use a Modular Monolith
ADR-002
Model Inventory Separately from Product
ADR-003
Preserve Purchase-Time Price in Order Items
ADR-004
Isolate Payment Provider Behind an Integration Boundary

We could also record PostgreSQL choice, but PostgreSQL is already an established engineering-context constraint rather than an open architectural decision made by this project.

That distinction matters।


Existing Constraint vs Project Decision

Suppose company says:

All transactional Java services use PostgreSQL.

Then our project did not meaningfully choose:

PostgreSQL vs MongoDB

There is little value in a fake ADR pretending we evaluated databases।

We may document PostgreSQL as context in RFC।

But ADR should focus on actual decisions।


ADR-001: Use a Modular Monolith

Let's write our first ADR.


ADR-001: Use a Modular Monolith for the Initial Backend

Status

Accepted

Context

The Order Management Backend must support:

Product management

Inventory management

Order creation

Order history

Order cancellation

Payment

Order creation requires coordinated changes to:

Order
+
Inventory

Cancellation also requires:

Order
+
Inventory

to remain consistent।

Current product requirements do not require:

independent service scaling

independent team ownership

cross-region architecture

independent deployment of Product, Inventory, and Order

Splitting these capabilities into separate network services would introduce:

network failures

distributed consistency problems

additional deployments

cross-service observability

service-to-service API management

before those costs are justified।


Decision

Implement the initial Order Management Backend as:

one deployable Spring Boot application with clear internal capability boundaries.

The application will be organized logically around:

Product

Inventory

Order

Payment

Security

but these capabilities will communicate through normal in-process Java calls rather than network APIs।


Alternatives Considered

Independent Microservices

Possible structure:

Product Service
Inventory Service
Order Service
Payment Service

Rejected for v1 because the current requirements do not justify distributed-system complexity।

Unstructured Monolith

One application with no meaningful internal capability boundaries।

Rejected because it would make responsibilities harder to maintain as the system grows।


Consequences

Positive:

simple deployment

simple local development

local PostgreSQL transactions

lower operational complexity

easy in-process collaboration between capabilities

Trade-offs:

capabilities cannot be independently deployed

team must maintain internal module discipline

application may need decomposition later
if scale or ownership requirements change

Why This ADR Is Useful

Suppose future engineer asks:

Why isn't Inventory a separate service?

ADR-001 answers:

Because current workflows benefit from local transactions,
and no requirement justified distributed deployment.

This is different from:

Because we forgot about microservices.

The architecture is deliberate।


ADR-002: Separate Inventory from Product

Our second major decision came from domain modeling।


ADR-002: Model Inventory Separately from Product

Status

Accepted

Context

Product and Inventory are related, but their responsibilities differ।

Product changes because of:

price updates

name/details updates

activation/deactivation

Inventory changes because of:

stock adjustments

order creation

order cancellation

Inventory also has a critical invariant:

quantity >= 0

and requires concurrency-safe updates during ordering।

If quantity were treated merely as another Product field, catalog state and inventory lifecycle would become unnecessarily coupled।


Decision

Model:

Product

and:

Inventory

as separate domain concerns within the same application।

Product owns:

current product information

current price

product ordering status

Inventory owns:

current available quantity

quantity-related rules

Inventory remains associated with Product।


Alternatives Considered

Store Quantity Directly on Product

Example:

Product
├── name
├── price
├── active
└── quantity

Benefit:

simpler data model initially

Rejected because Inventory has separate mutation patterns, rules, and concurrency concerns।


Consequences

Positive:

clearer responsibility

inventory-specific rules stay localized

order workflow can treat inventory explicitly

future inventory changes are less coupled to product metadata

Trade-offs:

additional domain/persistence concept

some reads may need Product and Inventory together

This trade-off is acceptable।


Separation Does Not Mean Separate Service

Important:

ADR-002 says:

separate domain concern

not:

separate microservice

Inventory and Product still live inside the same application।

Domain separation and deployment separation are different decisions।


ADR-003: Preserve Purchase-Time Price

This is one of the most important data decisions in the system।


ADR-003: Preserve Purchase-Time Price in Order Items

Status

Accepted

Context

Product prices can change over time।

Example:

Monday:
Product P1 = €20

Customer creates:

Order O1
P1 × 2

Later:

Friday:
Product P1 = €25

Requirement:

Historical Order O1 must continue to represent the price that applied when the order was created.

If historical orders load price only from the current Product record, O1 would appear to change from:

€40

to:

€50

without the order itself changing।

That would make historical order data incorrect।


Decision

Each Order Item will preserve the Product price used when the Order is created।

Conceptually:

OrderItem
├── product reference
├── quantity
└── purchase-time unit price

Current Product price remains owned by Product।

Historical purchase-time price remains owned by Order Item।


Alternatives Considered

Always Read Current Product Price

Rejected because historical Order values would change when Product price changes।

Snapshot the Entire Product

Possible approach:

product name
description
price
other fields

inside Order Item।

Rejected for current scope because the requirement only establishes historical pricing as necessary।

We will not snapshot unrelated Product data without a requirement।


Consequences

Positive:

historical price remains stable

order totals remain historically meaningful

Product price can change independently

Trade-offs:

price exists in both current Product state
and historical Order Item state

This is intentional because the two values represent different business facts।


Duplication Is Not Always Duplication

This ADR teaches an important modeling principle।

Suppose:

Product.price = €25

and:

OrderItem.unitPrice = €20

These are not duplicate values in the semantic sense।

They mean:

Product.price
→ price now
OrderItem.unitPrice
→ price when this order was created

Same data type।

Different fact।


ADR-004: Isolate Payment Provider

The external Payment Provider is our clearest system boundary।


ADR-004: Isolate Payment Provider Protocol Behind an Integration Boundary

Status

Accepted

Context

Order payment needs to communicate with an external provider।

Provider interaction includes:

HTTP endpoint

authentication

request schema

response schema

provider-specific status codes

timeouts

provider errors

Order business logic should reason about:

Is the order payable?

Did payment succeed?

Did payment fail?

Can the successful result safely be applied?

If provider-specific HTTP logic is placed directly inside Order application/domain code, payment protocol and business rules become tightly coupled।


Decision

Introduce an application-facing payment boundary।

Conceptually:

PayOrder
    ↓
PaymentGateway
    ↓
ProviderPaymentClient
    ↓
External Payment Provider

The application layer depends on provider-independent payment outcomes।

Provider-specific protocol remains inside integration code।


Alternatives Considered

Direct Provider HTTP Call from Order Service

Benefit:

less code initially

Rejected because it combines:

order business logic

HTTP integration

provider protocol

error translation

in the same component।

Generic Multi-Provider Plugin Framework

Rejected because only one payment provider is currently required।

We need an integration boundary, not an extensibility framework।


Consequences

Positive:

provider details remain isolated

Order logic is easier to understand

provider integration can be tested separately

provider protocol changes have smaller impact

Trade-offs:

adds an additional abstraction boundary

requires translating provider responses
into application-level outcomes

This complexity is justified by the external-system boundary।


Should "Use PostgreSQL" Be an ADR?

In our project, no।

Why?

Because PostgreSQL was already given as engineering context।

ADR should not manufacture a decision that did not actually exist।

If we were choosing between realistic storage options, then an ADR could be appropriate।

Good ADR practice requires honesty about decision context।


Should "Use Spring Boot" Be an ADR?

Same answer।

Spring Boot is an established project/company constraint।

We document it in engineering context and RFC।

No need to write:

ADR-005:
Use Spring Boot because Spring Boot is good.

That would add little value।


Should Every Domain Decision Become an ADR?

No।

For example:

Order contains Order Items.

This is central domain modeling but sufficiently natural from requirements।

It may not need an independent ADR।

Meanwhile:

Historical price is copied into Order Item
instead of always reading Product.price.

has a non-obvious trade-off and may be questioned later।

That makes it a better ADR candidate।


A Practical ADR Test

Before writing an ADR, ask:

Could a reasonable engineer later question
why this design exists?
Would changing it affect multiple parts
of the system?
Were there realistic alternatives?
Would losing the original reasoning
create risk?

If mostly yes, ADR may be useful।


Bad ADR: Implementation Detail

Example:

# ADR: Use `HashMap` in OrderCalculator

Usually not architectural।

Unless some exceptional system constraint makes this decision strategically important, it belongs in code or PR context।


Bad ADR: No Real Alternative

# ADR: Orders should have IDs

Identity is fundamental to the requirement।

There is no meaningful architectural decision to preserve here।


Bad ADR: Decision Without Context

# ADR: Use modular monolith

We will use a modular monolith.

This document provides almost no future value।

Important part is:

why

and:

what trade-offs follow

Bad ADR: Essay

The other extreme:

17 pages explaining the history of microservices

before saying:

we use one application

ADR should remain focused।

If extensive analysis is needed, that belongs in an RFC or linked design document।


Consequences Matter

A weak ADR often records only benefits।

Example:

Decision:
Use modular monolith.

Consequences:
It's simpler.

Every meaningful architecture decision has trade-offs।

Better:

Benefits:
local transactions, simpler operations.

Costs:
no independent deployment,
requires internal modular discipline.

Architecture is trade-off management।


Consequence Does Not Mean "Disadvantage Only"

Consequences include:

benefits

costs

new constraints

future implications

Example ADR-003:

Historical pricing preserved.

Positive।

But:

Price exists in two places with different semantics.

also something future engineers must understand।


ADR Alternatives Should Be Realistic

For modular monolith, realistic alternative:

independent microservices

For Inventory modeling:

quantity inside Product

For historical pricing:

always read current Product price

No need to list:

blockchain
serverless functions
graph database

if nobody seriously considered them।


Rejected Alternative Does Not Mean Bad Technology

When ADR says:

Microservices rejected

it means:

rejected for this context

not:

microservices are bad

Good architectural reasoning is contextual।


ADRs and Pull Requests

Suppose an implementation PR adds historical price:

BACKEND-110
Model Order and Order Item Domain

PR can reference:

ADR-003
Preserve Purchase-Time Price in Order Items

Reviewer can understand why the field exists।

Traceability:

Requirement
    ↓
RFC
    ↓
ADR
    ↓
Ticket
    ↓
PR
    ↓
Code

ADRs and Code Comments

Should we repeat entire ADR reasoning inside code comments?

Usually no।

Bad:

// We store unitPrice because...
// 20 lines copied from ADR...

Better code expresses concept clearly:

private BigDecimal unitPrice;

and design documentation holds deeper historical reasoning।

A short reference may occasionally be useful for non-obvious implementation constraints, but avoid turning source code into duplicated documentation।


ADRs and Future Refactoring

Suppose future engineer wants to merge Inventory into Product।

Before changing architecture, ADR-002 gives context:

Inventory was separated because its lifecycle,
mutation pattern, and concurrency concerns differ.

Now engineer can ask:

Are those reasons still true?

If yes, merging may be a regression।

If business/system has changed, a new ADR can supersede it।

This is exactly the value of decision history।


Superseding an ADR

Suppose future requirement introduces separate Inventory team, independent scaling, and very high write load।

Team decides Inventory should become a separate service।

New ADR:

ADR-010:
Extract Inventory into an Independent Service

Context:

traffic increased

team ownership changed

independent deployment now required

Then:

ADR-001

or relevant architectural record may become partially superseded।

Architecture evolves based on changed constraints।


ADRs Should Describe the World at Decision Time

This is important।

Do not rewrite old ADR with current facts。

The ADR should preserve:

What did we know then?

What requirements existed?

Why was the decision reasonable then?

Future engineers can understand architecture evolution rather than judging old decisions with new context।


Proposed ADR Directory

Our repository can now contain:

docs/
├── rfcs/
│   └── 001-order-management-architecture.md
│
├── adr/
│   ├── 001-use-modular-monolith.md
│   ├── 002-separate-inventory-from-product.md
│   ├── 003-preserve-purchase-time-price.md
│   └── 004-isolate-payment-provider.md
│
└── incidents/

This remains lightweight।

Four ADRs are enough for current design।


ADR Naming

Good filenames describe decision:

001-use-modular-monolith.md

Better than:

decision1.md

or:

architecture.md

A future engineer should be able to browse the directory and understand what decisions exist।


ADR Template

For this project, we can standardize a simple template:

# ADR-XXX: Title

## Status

Proposed | Accepted | Superseded

## Context

What problem or decision requires resolution?

## Decision

What did we decide?

## Alternatives Considered

What realistic alternatives were considered?

## Consequences

What benefits, costs, and constraints follow?

That's enough for most decisions।


When to Create the ADR?

Ideally around the time the decision is made।

Not six months later when nobody remembers the reasoning।

Typical flow:

Design discussion
      ↓
Decision
      ↓
ADR
      ↓
Implementation

The ADR may originate from an RFC review।


Proposed vs Accepted

During design discussion:

Status: Proposed

Once team agrees:

Status: Accepted

We should not mark unresolved decisions as Accepted just because someone created a document।

Status should mean something।


What if Reviewers Disagree?

Suppose ADR proposes modular monolith।

Reviewer argues:

Inventory requires independent scaling from day one.

Then team needs evidence।

Questions:

What scale do we expect?

What operational requirement demands independent scaling?

What consistency complexity will separate services create?

Is that requirement confirmed?

ADR discussion should focus on trade-offs and context, not preferences।


ADRs Are Not Authority

Once Accepted, ADR should guide implementation।

But it is not impossible to challenge।

If new evidence appears:

requirement changed

operational constraint changed

original assumption proved false

architecture can evolve।

The process is:

challenge with evidence
      ↓
new decision
      ↓
new ADR / supersede old ADR

not silently ignore the record।


ADR and YAGNI

ADRs can help enforce scope discipline।

ADR-004 says:

Use one PaymentGateway boundary.

It does not say:

Build a provider plugin ecosystem.

If future code starts introducing:

PaymentProviderRegistry

DynamicPaymentStrategyFactory

reviewer can ask:

Which requirement changed?

Decision record reinforces current design intent।


ADR and Non-Goals

Non-goals are usually more natural in RFC than every ADR।

However, ADR can explicitly clarify boundaries when useful।

For ADR-001:

Decision does not imply
that future service extraction is forbidden.

It simply says:

v1 remains one deployable application.

This prevents decision being interpreted too broadly।


ADR and Technical Debt

Suppose team makes a temporary compromise:

Use synchronous provider call for v1
because provider does not yet support webhooks.

If the decision has meaningful consequences and expected future reconsideration, an ADR could record:

why we accepted this limitation

and:

what condition should trigger reconsideration

ADR can make intentional debt visible।

But not every TODO requires an ADR।


Decision Drivers

Sometimes ADR benefits from explicitly identifying decision drivers।

For example ADR-001 drivers:

local transaction requirements

current team size

operational simplicity

lack of independent-scaling requirement

We could add a Decision Drivers section if helpful।

For our lightweight template, these can stay in Context।


Architecture Decision vs Business Decision

ADR records technical architecture decisions।

Business rule:

Paid orders cannot be cancelled in v1.

comes from product requirement।

We do not need an architecture ADR saying:

ADR: Paid orders cannot be cancelled.

That is a business/product decision, better captured in requirements/acceptance criteria।

But technical consequence:

Order lifecycle is modeled using explicit states
rather than independent `paid` and `cancelled` booleans.

could become an architectural/modeling decision if sufficiently important।


Avoid Using ADR to Override Product Requirements

Engineering cannot write:

ADR:
Allow paid-order cancellation.

if product requirement says otherwise।

An ADR records technical decisions within agreed requirements।

If business behaviour needs change, product clarification comes first।


What About Inventory Concurrency Strategy?

We have not chosen exact mechanism yet।

Could it become an ADR?

Potentially yes।

Suppose later we choose:

pessimistic row locking

instead of:

optimistic locking

or:

atomic conditional update

If that decision significantly affects persistence behaviour, concurrency, and performance, it may deserve an ADR।

But we should not write it now because the decision has not been made।


Do Not Create Placeholder ADRs

Bad:

ADR-005:
Inventory Locking Strategy

Decision:
TBD

That is not a decision record।

Open questions belong in RFC or backlog until a decision exists।

Once resolved, create ADR if warranted।


Payment Idempotency May Become an ADR

Same for payment।

Once we understand actual provider capability, we may make a durable decision about:

idempotency keys

payment-attempt persistence

provider-reference handling

That may deserve another ADR।

But current information is insufficient।

So we intentionally defer it।

This is better than hallucinating architecture।


Architecture Decision Review Checklist

Before accepting an ADR, ask:

Is the problem/context clear?

Is the decision explicit?

Does it align with requirements?

Were realistic alternatives considered?

Are trade-offs honestly described?

Is this decision important enough to record?

Are we documenting an actual decision,
not an unresolved question?

Would a future engineer understand
why this exists?

A Weak ADR Example

# ADR-005: Use PaymentGateway

## Status

Accepted

## Context

We need payments.

## Decision

Use PaymentGateway.

## Consequences

Cleaner code.

This provides little insight।

Why gateway?

What problem does it solve?

What alternative was rejected?

What complexity does it add?


A Better ADR

# ADR-004: Isolate Payment Provider Behind an Integration Boundary

## Context

Order payment depends on an external HTTP provider.
Provider-specific request formats, error codes, and timeouts
should not become part of Order business logic.

## Decision

Application payment workflows depend on a provider-independent
PaymentGateway boundary. Provider HTTP details remain in the
integration implementation.

## Alternatives Considered

Call the provider directly from Order application logic.

## Consequences

Provider changes have less impact on Order logic,
but the application needs an additional mapping boundary.

Short, but useful।


ADRs Should Be Boring to Read

That is often a good sign।

An ADR is not marketing copy।

It should be:

clear

specific

factual

easy to scan

Future engineer may open it during an incident or refactor।

Clarity matters more than style।


Our Current Decision Set

After Module 2 design work, our decision history now looks like:

RFC-001
Initial Order Management Backend Architecture

supported by:

ADR-001
Use a Modular Monolith
ADR-002
Model Inventory Separately from Product
ADR-003
Preserve Purchase-Time Price in Order Items
ADR-004
Isolate Payment Provider Behind an Integration Boundary

This is enough documentation for the major current architectural choices।


What We Are Not Recording Yet

No ADR yet for:

inventory locking mechanism

because unresolved।

No ADR yet for:

payment idempotency strategy

because provider behaviour is unresolved।

No ADR for:

Java / Spring Boot / PostgreSQL

because those are given engineering context rather than meaningful project-level choices।

No ADR for:

class names
package naming
DTO shapes

because those are not durable architectural decisions at this point।


Engineering Principle

The core principle from this lesson:

Code preserves implementation. ADRs preserve the reasoning behind important architectural decisions.

Another:

Record decisions that future engineers may reasonably want to challenge, understand, or reverse.

And:

Do not manufacture ADRs for decisions that were never actually open, and do not record unresolved questions as if they were decisions.


Summary

In this lesson, we learned that:

  • ADR means Architecture Decision Record.

  • ADR captures one important technical decision and its reasoning.

  • RFC is broader; ADR is decision-focused.

  • ADRs should remain short and useful.

  • ADRs are appropriate for durable, non-obvious, costly-to-reverse decisions.

  • Not every implementation choice needs an ADR.

  • Existing engineering constraints do not need fake decision records.

  • Accepted ADRs should not be silently rewritten when architecture changes.

  • New decisions can supersede older ADRs while preserving history.

  • Alternatives should be realistic and contextual.

  • Consequences should include both benefits and costs.

  • Product requirements and architecture decisions are different artifacts.

  • Unresolved technical questions remain in RFC/backlog until an actual decision exists.

  • Inventory concurrency and payment idempotency may produce future ADRs after their designs are known.

  • We created four current ADRs:

    • modular monolith
    • Inventory separated from Product
    • purchase-time price preserved in Order Items
    • Payment Provider isolated behind an integration boundary
  • ADRs connect design reasoning to tickets, PRs, code, and future refactoring.

Next lesson:

Reviewing and Revising a Technical Design

There we will simulate an actual engineering design review of RFC-001, identify weak assumptions and unresolved questions, respond to reviewer feedback, revise the proposal where necessary, and decide when the design is sufficiently ready for implementation.