Designing the System
Architecture Decision Records
আপনি একটি 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.