How to buildOctober 11, 202611 min read
Software architectureStep 1 of 9

Software architecture: what it is and which decisions belong to it

In short

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.

FundamentalsLevel: IntermediateArchitecture Fundamentals

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
  1. 01Architecture is the decisions that are costly to change
  2. 02Quality attributes: what the system must withstand
  3. 03Trade-offs instead of "best practices"
  4. 04Modules and boundaries
  5. 05Drawing the system: the C4 model
  6. 06ADR: recording decisions and their reasons
  7. 07Evolution: architecture grows with the product

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
Examples for an online salon booking service

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.

  1. 01System ContextThe whole system, its users and external systems. Readable by non-technical people
  2. 02ContainerApplications and data stores inside the system, their technologies and the protocols between them
  3. 03ComponentModules inside one container and their responsibilities
  4. 04CodeClasses and functions. Better generated by an IDE than drawn by hand
The four C4 levels: each one zooms into a single element of the previous level

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.

  1. 01Salon client
  2. 02Online booking service
  3. 03Telegram Bot API
System context diagram (simplified): the client books through the service, the service sends reminders via Telegram. The salon admin and the payment provider are relationships at the same level

The container level zooms into the online booking service: which runnable parts it consists of and how they communicate.

  1. 01Client browser
  2. 02Web application (Next.js)
  3. 03Booking API (Node.js)
  4. 04Database (PostgreSQL)
Container diagram: the path of a booking request. The booking API also sends reminders to the Telegram Bot API

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
—
Elements of the container diagram — what you label on the diagram itself
  • 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.

  1. Title

    A number and a short noun phrase: "ADR 0002: Modular monolith for the booking service".

  2. Context

    The forces at play: technical, organisational, deadlines, team. Value-neutral language, facts only.

  3. Decision

    The response to these forces, in full sentences and active voice: "We will…".

  4. Status

    "Proposed" until the team agrees, then "Accepted". Later "Deprecated" or "Superseded" with a link to the new ADR.

  5. Consequences

    The resulting context after the decision: what gets easier and what gets harder. All consequences, not just the positive ones.

docs/adr/template.md
# 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.
A template in Michael Nygard's format, sections in the order of the original article
docs/adr/0002-modular-monolith.md
# 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.
A teaching example ADR for the booking service. Both files pass markdownlint-cli2 with default settings

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.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.