Fundamentalsarchitecturecomponentsboundariesdependenciesdata flow

What Is Software Architecture?

Architecture is the set of decisions that are expensive to change — which components exist, where their boundaries are, which direction dependencies point, how data and failures move between them — and the skill is choosing the simplest structure that meets a measured requirement.

▶ InteractiveInterview question
Progress
What problem does this solve?

A system of any size has to be split into parts that people can build, deploy, scale and fix independently. Architecture is the discipline of choosing those parts and the rules between them, so that the split helps rather than hurts.

The vocabulary, tied to one small system

Take a small but real system: a browser talks to an API, the API reads and writes Postgres, caches hot reads in Redis, and pushes slow work (sending an email through a third-party provider) onto a queue that a worker drains. Every architectural term you will meet is visible in that picture, and it is worth being precise about each one, because interviews and design reviews turn on them.

A component is a part with one responsibility that can be reasoned about on its own: the API, the worker, the database. A boundary is the line around a component that other components may not cross except through its interface — the HTTP routes of the API, the message schema of the queue, the SQL schema of the database. A dependency is an arrow: the API depends on Postgres, the worker depends on the email provider; the direction matters because a component can only be as reliable, fast and stable as the things it depends on. Data flow is the path a piece of information takes (an order travels browser → API → Postgres → queue → worker → email provider), and communication is how each hop is made: synchronous HTTP, an in-process call, or an asynchronous message. Deployment is which components ship together and where they run. A failure domain is the set of things that go down together: if Redis dies, what else dies? Scalability is which components can be copied when load grows, and maintainability is how many components a typical change touches.

  • Component: the API, the worker, Postgres, Redis, the queue, the email provider — each has one job.
  • Interface: POST /orders is the API’s interface; the SendEmail message schema is the worker’s; the tables are the database’s.
  • Dependency direction: API → Postgres, Worker → Email provider. Nothing depends on the browser; everything depends on the database.
  • Failure domain: the worker and the email provider share one — the API does not, because the queue sits between them.
  • Communication: browser → API is synchronous HTTP; API → worker is asynchronous via the queue; worker → provider is synchronous HTTPS with a timeout.
A small system, labelled
HTTPS, synchot readsSQL, syncSendEmail, asyncconsumeHTTPS, 5 s timeoutBrowser (client)API (component)Postgres (state)Redis (cache)Queue (boundary)WorkerEmail providerworker + provider = one failure domain
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

The four levels

"Architecture" is used for four different things, and confusing them causes real arguments: someone proposes hexagonal packages while the actual problem is that the database is at 95% CPU. Code structure is how one codebase is organised into packages, layers and classes — see Layered Architecture and Clean Architecture. Application architecture is how one deployable is shaped: a Monolithic Architecture, a Modular Monolith, or a set of Microservices. System architecture is how deployables, data stores, queues and external systems connect, and what happens when one of them fails. Infrastructure architecture is what all of that runs on: regions, networks, load balancers, container orchestration, DNS.

A decision at one level constrains the levels around it. Choosing microservices (application) forces service discovery and a gateway (system) and a container platform (infrastructure). Choosing a single Postgres (system) caps the write throughput regardless of how clean the packages are (code). When a proposal is on the table, the first question is which level it is really about.

The four levels and the question each answers
LevelUnitThe questionExample decisionWho feels it when wrong
Code structurepackage, module, classWhere does this logic live and what may it import?Domain code must not import the ORMEvery developer, every day
Applicationone deployableHow is this process shaped and split?Modular monolith with a module per business capabilityTeams, at deploy and refactor time
Systemservices, stores, queuesHow do deployables connect and fail together?Orders publish events; email is async behind a queueOn-call, during incidents
Infrastructuremachines, networks, regionsWhat does it all run on and how is it operated?Two availability zones behind one load balancerOps, and users during a zone outage

Architecture is the expensive-to-change decisions

Not every decision is architectural. Renaming a function, choosing a date library, or adding a column is cheap to reverse. Splitting a database into two, changing an API from synchronous to event-driven, or moving state out of a process are expensive to reverse — they touch many components and often require data migration. Those are the decisions architecture is about, and the reason to spend design time on them is precisely that you will live with them.

This gives a practical rule: the simplest architecture that meets a measured requirement wins. Not the one that would cope with a hypothetical 100× — the one that meets today’s p99, today’s throughput, today’s team size, with a known next step when a metric says it is time. Every box added to a diagram must be able to answer "which measured problem put you here?" — the interview question why-did-you-add-that-box is exactly this. The next lesson, From a Simple App to a Scaled System, shows a system growing one measured problem at a time.

  • Cheap to change: library choice, function names, an index, an extra column.
  • Expensive to change: number of databases, sync vs async between components, where session state lives, service boundaries.
  • Design time should be proportional to the cost of being wrong, not to how interesting the problem is.

How to read a diagram like an engineer

A diagram is a claim, and the way to test it is to ask it questions. For every arrow: is this synchronous (the caller waits and inherits the callee’s latency and failures) or asynchronous (the caller continues; the work happens later and may be duplicated)? For every box: what state lives here, and what happens to in-flight requests if it dies right now? For every pair of boxes: can they be deployed and scaled independently, or do they share a database, a deploy pipeline or a thread pool that quietly couples them?

The same questions drive the rest of this domain. Scaling (Scale This System) is about which boxes can be copied. Reliability (Reliability Patterns) is about what an arrow does when the far end is slow. Observability (Distributed Tracing) is about seeing which arrow the time went into. Distributed data (Distributed Transactions) is about what happens when a write has to cross two boxes. Architecture is not a vocabulary; it is a set of questions you learn to ask of a picture.

Questions to ask of every diagram
For each arrow   : sync or async?  timeout?  retried?  idempotent?
For each box     : what state is here?  what dies with it?  can it be copied?
For each boundary: who owns the schema on the other side?  who deploys it?
For the whole    : which measured number would change this picture?

Key points

  • Components, boundaries, interfaces, dependencies, data flow, communication, deployment, failure domains: each is a concrete question about a real diagram, not a vocabulary word.
  • Four levels — code structure, application, system, infrastructure — and most bad arguments come from mixing them.
  • Architecture is the set of expensive-to-change decisions; spend design time in proportion to the cost of being wrong.
  • The simplest architecture that meets a measured requirement wins; every box must name the measured problem that put it there.
  • Read diagrams by interrogating arrows (sync/async, timeout, retry) and boxes (state, failure domain, copyable).

Anatomy of a system: click any part

Anatomy of a system: click any part
Every runtime component: services, data stores, middleware and the third party we depend on.
System level
consumesWeb / mobile clientEmail / PDF workerLoad balancerOrder serviceCatalog servicePayment providerRedisQueuePostgreSQL

The system level is what an on-call engineer draws on a whiteboard: every process, store and third party, and which of them can take the others down. This is where failure domains and single points of failure become visible.

Component
PostgreSQL
Responsibility
System of record for users, orders, products, payments.
Interface (what it exposes)
SQL over TCP :5432, one schema
Depends on (out)
nothing — a leaf
Depended on by (in)
Order serviceCatalog serviceEmail / PDF worker
Failure domain (dies with it)
Order serviceCatalog serviceEmail / PDF workerLoad balancerWeb / mobile client
Deployment unit
Managed instance; migrations ship with the services
How it scales
Vertically first, then read replicas; sharding is a rewrite
If PostgreSQL dies, 5 other components fail with it (Order service, Catalog service, Email / PDF worker, Load balancer, Web / mobile client). That is its failure domain, computed only from the arrows: every hard edge pointing at it is a path for the outage to travel. More than half the system sits in this domain — a single point of failure that deserves redundancy (or a soft edge) before anything else gets attention.
View level

How data moves through it

One request or event, hop by hop.

  1. 1Browser → API: POST /orders over HTTPS; the browser waits for a response.
  2. 2API → Redis: check for a cached product price; on a miss, fall through.
  3. 3API → Postgres: INSERT the order in one transaction; the commit is the moment the order exists.
  4. 4API → Queue: publish SendEmail{orderId}; the API returns 201 without waiting for the email.
  5. 5Queue → Worker → Email provider: the worker consumes, calls the provider with a 5 s timeout, acknowledges the message on success.

When to use — and when not

Use it when
  • Before any design discussion: agree on which level (code, application, system, infrastructure) the proposal is about.
  • When reviewing a diagram: interrogate each arrow and box rather than judging the picture as a whole.
  • When a change touches more than one deployable or moves state — that is when an architectural decision is being made.
Avoid it when
  • Do not run an architecture review for cheap-to-reverse decisions (a library, a column, a function name); the ceremony costs more than the risk.
  • Do not choose structure from a target picture ("we will need microservices eventually"); choose it from the current measured problem.

Tradeoffs

Complexity
low → high
Ops cost
low → high
Latency
low → high
Consistency
weak → strong
Scalability
poor → strong

Ratings describe the browser → API → database baseline: minimal machinery, strong consistency from one database, and a single-box scaling ceiling. Every later lesson trades some of that consistency and simplicity for capacity.

How it fails

  • Mixing levels: a team rewrites packages into hexagonal layers while the real bottleneck is a database at 95% CPU.
  • Boxes without causes: an API gateway, a queue and a cache added because the reference architecture had them, each a new failure domain nobody asked for.
  • Unread arrows: a synchronous call to a third party on the checkout path, so a 30 s provider timeout becomes a 30 s checkout.
  • Invisible coupling: two "independent" services sharing a database schema, so a migration in one breaks the other.

How it scales

  • The baseline scales vertically first (a bigger box), which changes no arrows and adds no failure domains.
  • The first horizontal step is copying the stateless component (the API) behind a load balancer; that only works if state has already been externalised to Postgres and Redis — see Stateless vs Stateful Services.
  • The database is the shared, stateful box and becomes the ceiling; the ladder of Scaling from One User to Millions applies before any application-level split.

How it interacts with databases, queues, caches, APIs and external systems

  • Database: the single source of truth; every component that needs current state reads it through the API, not directly.
  • Cache: an optimisation on the read path that must be safe to lose — the API falls through to Postgres on a miss or a Redis outage.
  • Queue: the boundary that separates the API’s failure domain from the worker’s; it also introduces at-least-once delivery, so the worker must tolerate a duplicate SendEmail.
  • External API: the email provider is outside your control; it is called only from the worker, with a timeout and a retry budget, never from the request path.