Software architecture is the set of significant decisions about a system that are costly to change: how it is split into modules, where it stores data, which systems it integrates with and how it is deployed. Every such decision is a trade-off between quality attributes: reliability, security, performance, cost and ease of change. Teams show these decisions with diagrams (for example, the C4 model) and record them, with reasons, in ADRs — short documents kept in the repository.
After this you will be able to
- tell an architectural decision from an implementation detail by how much it costs to change
- link project decisions — module boundaries, data storage, integrations, deployment — to quality attributes and name the price of each
- draw C4 system context and container diagrams with labelled relationships
- record a decision as an ADR using Michael Nygard's template and supersede an outdated decision with a new ADR
Verified with
C4 model (c4model.com)ADR: Michael Nygard format (2011)markdownlint-cli2 0.23.3 (markdownlint 0.41.1)
Checked against docs: October 11, 2026
Contents
Architecture is the decisions that are costly to change
A project is made of hundreds of decisions: what to call a function, which date library to use, where to store data, how applications talk to each other. Most of them can be revisited in an hour. A few cannot: changing the database, splitting the application into services or switching the sign-in mechanism touches all the code and all the data. Those are the decisions we call architecture.
In his column "Who Needs an Architect?" (IEEE Software, 2003), Martin Fowler discusses two definitions by Ralph Johnson. First: architecture is the shared understanding that the expert developers have of the system design. Second: it is the decisions you wish you could get right early in a project. People want to get them right early because they perceive them as hard to change — hence the working definition "things that people perceive as hard to change" and Fowler's conclusion: architecture is about the important stuff, whatever that is.
Michael Nygard gave a practical filter in his 2011 article on ADRs: architecturally significant decisions are those that affect the structure, non-functional characteristics, dependencies, interfaces or construction techniques of a system.
One application or several services
- Architectural?
- Yes
- Why
- Changes build, deployment, monitoring and transaction boundaries
PostgreSQL or a document database for bookings
- Architectural?
- Yes
- Why
- Queries and the data model are rewritten, a migration is needed
How we learn about payments: webhook or polling
- Architectural?
- Yes
- Why
- An interface with an external system, affects reliability
Where sessions live and how sign-in is checked
- Architectural?
- Yes
- Why
- Security, touches every request
Which library formats dates
- Architectural?
- Usually not
- Why
- Changes in one place if hidden behind your own function
Variable names and code style
- Architectural?
- No
- Why
- Changed by refactoring and a linter
Quality attributes: what the system must withstand
Functional requirements say what the system does: a client books a stylist, an admin moves a booking. Quality attributes — also called non-functional requirements — say how it does it: how reliably, securely, quickly and at what cost. Architecture mostly answers these: the same "book a slot" feature can be built a dozen ways with different properties.
A handy frame is the five pillars of Microsoft's Azure Well-Architected Framework: reliability, security, cost optimization, operational excellence and performance efficiency. It was written for the cloud, but its questions fit any system. We add a sixth attribute — changeability, i.e. how much a change costs. Fowler stresses that high internal quality speeds up feature delivery rather than slowing it down: there is less cruft getting in the way of understanding the code.
Reliability
- Question to the system
- What happens when one part fails?
- Example requirement for the booking service
- A booking is saved even if Telegram does not respond; the reminder goes out later
Security
- Question to the system
- Who can access what?
- Example requirement for the booking service
- A stylist sees only their own bookings; client phone numbers never reach the logs
Performance
- Question to the system
- How long does a user wait under load?
- Example requirement for the booking service
- Free slots open without delay during the evening booking peak
Cost
- Question to the system
- How much does running the system cost?
- Example requirement for the booking service
- No servers sitting idle for the sake of rare peaks
Operations
- Question to the system
- How do we learn about a problem and ship a fix?
- Example requirement for the booking service
- Errors are visible in monitoring, a release is rolled back with one command
Changeability
- Question to the system
- How much does a new feature cost?
- Example requirement for the booking service
- A new payment method is added without touching the bookings module
Trade-offs instead of "best practices"
Quality attributes conflict: improving one almost always costs you another. The Well-Architected Framework has a separate trade-offs page for every pillar. A few examples from the performance section:
- A cache speeds up responses, but the longer a copy lives, the more likely it reflects a stale state: a cached authorization decision may keep letting in a user who has already been disabled
- Components added for speed — a message bus, a load balancer, a cache — enlarge the attack surface and the cost, and must be made reliable themselves
- Sharding and denormalization speed up work with large datasets but complicate consistency and routine operations such as backups
- Packing several applications densely onto one platform saves resources but widens the blast radius of a failure
Asking "what is right?"
- Looks for a universal answer
- Copies the architecture of big companies
- Adds components "for future growth"
- Never says when to revisit the decision
Asking "what do we pay?"
- Names the attribute that matters most right now
- Lists what gets worse
- Checks against team size and real load
- Records the condition for revisiting the decision
That is why "this is better" sounds suspicious in architecture. The working wording is "for our load and our team we choose X for the sake of attribute A and accept cost B". That is exactly what goes into an ADR, covered below.
Modules and boundaries
A module is a part of the system with one area of responsibility and an explicit public interface. A module boundary answers the question of what the outside world may use and what is internal. For the booking service, natural modules match the business language: Bookings, Schedule, Notifications, Payments.
- A module owns its data: only Payments reads and changes the payment tables
- Neighbours reach it through a public interface — functions, events or an API — not through internal files
- Dependencies between modules point one way and do not form a cycle
- A change inside a module does not require edits in its neighbours
- A module can be tested with its neighbours replaced by stubs
A boundary does not have to be a network boundary. Modules can live in one application — a modular monolith — or be separate services. In his "MonolithFirst" note, Fowler writes that almost all successful microservice stories started with a monolith that grew too big and was broken up, and that even experienced architects find it hard to get boundaries right at the start. Hence the order: boundaries inside the code first, network boundaries later if there is a reason.
Inside a backend module the code is usually split into layers as well: a controller handles HTTP, a service holds business rules, a repository works with the database. That is the next zoom level, covered in the lesson on backend application layers.
Drawing the system: the C4 model
You need to be able to show the architecture: to a new developer, to a client, to yourself six months later. Simon Brown's C4 model draws a system like a map with zoom levels — four levels from the big picture down to code. The model is notation- and tooling-independent: a whiteboard works as well as any diagramming tool.
- 01System ContextThe whole system, its users and external systems. Readable by non-technical people
- 02ContainerApplications and data stores inside the system, their technologies and the protocols between them
- 03ComponentModules inside one container and their responsibilities
- 04CodeClasses and functions. Better generated by an IDE than drawn by hand
c4model.com recommends system context and container diagrams for all teams. It suggests drawing component diagrams only if they add value, and generating them automatically for long-lived documentation. It does not recommend code diagrams for long-lived documentation because IDEs generate them on demand. Let us start with the context of the booking service.
- 01Salon client
- 02Online booking service
- 03Telegram Bot API
The container level zooms into the online booking service: which runnable parts it consists of and how they communicate.
- 01Client browser
- 02Web application (Next.js)
- 03Booking API (Node.js)
- 04Database (PostgreSQL)
Web application
- Type and technology
- Container: Next.js
- Responsibility
- Booking screens for clients and the schedule for the admin
- Relationships
- Calls the booking API (JSON over HTTPS)
Booking API
- Type and technology
- Container: Node.js
- Responsibility
- Bookings, Schedule, Notifications and Payments modules; background reminder sending
- Relationships
- Reads and writes data (SQL); sends reminders to the Telegram Bot API (HTTPS)
Database
- Type and technology
- Container: PostgreSQL
- Responsibility
- Bookings, slots, clients, reminder queue
- Relationships
- —
Telegram Bot API
- Type and technology
- External system
- Responsibility
- Delivers reminders to the client
- Relationships
- —
- The diagram has a title stating its type and scope: "Container diagram: online booking service"
- There is a key explaining shapes, colours, line styles and arrows
- Every element has a type; containers and components also have a technology
- Every arrow is unidirectional and has a specific label: not "uses" but "sends reminders over HTTPS"
ADR: recording decisions and their reasons
A diagram shows what was built but not why. A year later a new developer sees the monolith and asks why it is not microservices. Michael Nygard described two bad outcomes: blindly accepting an old decision without understanding it, or blindly changing it and breaking whatever it was made for. His answer is the Architecture Decision Record (ADR), a short record of one architectural decision and its reasons.
ADRs live in the repository next to the code, in Markdown, numbered sequentially; numbers are never reused. Length: one or two pages — in Nygard's words, nobody ever reads large documents, and large documents are never kept up to date. The format has five parts.
Title
A number and a short noun phrase: "ADR 0002: Modular monolith for the booking service".
Context
The forces at play: technical, organisational, deadlines, team. Value-neutral language, facts only.
Decision
The response to these forces, in full sentences and active voice: "We will…".
Status
"Proposed" until the team agrees, then "Accepted". Later "Deprecated" or "Superseded" with a link to the new ADR.
Consequences
The resulting context after the decision: what gets easier and what gets harder. All consequences, not just the positive ones.
# ADR NNNN: Short noun phrase describing the decision
## Context
The forces at play: requirements, constraints, deadlines, team, load.
Facts only, value-neutral. Which options were considered.
## Decision
What we are doing. Full sentences in active voice: "We will…".
## Status
Proposed | Accepted | Deprecated | Superseded by ADR NNNN
## Consequences
What becomes easier and what becomes harder after the decision.
All consequences, not just the positive ones.# ADR 0002: Modular monolith for the booking service
## Context
An online booking service for salons: a client picks a service, a stylist
and a time slot, the admin sees the schedule, and the client gets a Telegram
reminder. The team is three developers shipping once a week. The Bookings,
Schedule, Notifications and Payments modules are identified, but their
boundaries have been tested with a single salon only. A booking must not be
lost when Telegram is unavailable.
## Decision
We will deploy the backend as one Node.js application with one PostgreSQL
database; the Next.js web front end remains a separate application.
Each module lives in its own folder, owns its own tables and exposes
only a public interface to other modules. A linter rule in CI forbids imports
of another module's internal files. Reminders are sent by a background job:
the booking is saved to the database immediately, and sending is retried
when Telegram fails.
## Status
Accepted, 2026-10-11. Supersedes ADR 0001 "Separate notification service".
## Consequences
One build and one deployment: monitoring, rollback and local runs are simpler.
Creating a booking and reserving a slot stay in one database transaction.
Modules cannot be scaled or released independently: a fix in Payments
requires releasing the whole application. Boundaries rely on discipline and
import checks; without them the monolith turns into a "big ball of mud".
If a module needs its own scaling or a separate team, we will extract it into
a service and record that in a new ADR that supersedes this one.Nygard's template is not the only one. The adr.github.io catalogue and Joel Parker Henderson's repository collect others: MADR with an explicit list of considered options and their pros and cons, Y-statements — a decision in one structured sentence, and the detailed Tyree and Akerman template. Consistency matters more than the template. You do not need an ADR for every decision: a temporary workaround, a prototype or an experiment with limited risk and cost does not need one.
Evolution: architecture grows with the product
Architecture is not approved once and for all. Fowler quotes an email from Johnson: there is no theoretical reason that anything is hard to change about software. Any one aspect can be made easy to change, but that makes the overall system a little more complex, and making everything easy to change makes the entire system very complex — and complexity is what makes software hard to change. So, in Fowler's view, one of an architect's most important tasks is to remove irreversibility where it is expensive — not to make everything flexible.
- Defer expensive decisions until you have enough information, but no longer: "we'll figure it out later" is a decision too, and worth recording
- Hide what is likely to change behind your own interface: the payment provider, the notification channel, file storage
- Revisit an ADR when the conditions in its Context change: the team or the load grew, a regulator added a requirement
- Update the context and container diagrams together with the code — they change rarely, so keeping them current is cheap
Next in this direction: the principles used to draw boundaries inside the code — SOLID — and then the design patterns that implement those boundaries in practice.
How this was verified2 · passedchecked 11 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.
Sources
- Martin Fowler — Software Architecture Guide
- Martin Fowler — Who Needs an Architect? (IEEE Software, 2003)
- Michael Nygard — Documenting Architecture Decisions
- Architectural Decision Records (adr.github.io)
- The C4 model for visualising software architecture
- Azure Well-Architected Framework — Performance Efficiency tradeoffs
