Learn Software Architecture
Eleven modules from what architecture is to observability. Every lesson answers the same eight questions — what it solves, when to use it, when to avoid it, the tradeoffs, how it fails, how it scales, how data moves, and how it talks to databases, queues, caches and external systems — and carries an interactive.
What architecture is — components, boundaries, dependencies, data flow, failure domains — and how a system evolves from browser → backend → database.
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.
A system grows from browser → backend → database into gateways, services and caches one measured problem at a time — CPU at 90%, p99 above 800 ms, four teams blocked on one deploy — and every step buys capacity by adding a problem you did not have before.
Monolith, modular monolith, microservices: what each solves, what each costs, and the decision between them.
A monolith is one deployable process containing every business capability — auth, users, orders, payments, notifications — sharing one database and one transaction scope; it is the cheapest architecture to build, run and debug, scales further than its reputation suggests, and is not automatically bad.
A modular monolith keeps one deployable and one database but enforces service-grade boundaries inside it — each module exposes a public API, owns its tables, and communicates through in-process events — so teams get independence in the codebase without paying for a network, and a module can later be extracted along a boundary that already exists.
Microservices split an application into independently deployed services that each own their data and talk over the network — buying team autonomy, independent releases and per-service scaling at the price of partial failure, lost transactions, contract versioning and an observability stack; adopt them for a measured organisational or scaling problem, or you build a distributed monolith.
Layered, Clean and Hexagonal: dependency direction, domain isolation, and when the abstraction stops paying for itself.
Presentation → Application → Domain → Infrastructure, with dependencies pointing one way only — a cheap, widely understood way to keep HTTP out of business rules and SQL out of controllers, until the layers become pass-through ceremony that adds files without adding decisions.
Concentric rings — Entities, Use Cases, Interface Adapters, Frameworks & Drivers — governed by one rule, source-code dependencies point inward, so business rules never import the web framework or the database; powerful where boundaries matter, and pure overhead in a system too small to have boundaries.
The application core exposes ports — interfaces it needs and interfaces it offers — and every external thing (REST, a database, a queue, a payment provider) is an adapter plugged into a port, so the domain is isolated from technology and any adapter can be swapped for an in-memory one in a test.
Event-driven architecture, queues, Kafka-style logs, and the sync-vs-async decision — with duplicates, ordering and replay taken seriously.
A producer records that something happened and stops caring who listens; consumers subscribe and react on their own clock — which buys decoupling and fan-out at the price of eventual consistency, duplicate delivery, ordering you must design for, and a "who owns the truth" question you must answer explicitly.
A queue turns "call Service B now" into "hand Service B a message it will process when it can" — buying failure isolation and a buffer for bursts, at the price of ack/retry/visibility-timeout semantics, dead-letter handling and a backlog you must watch.
A topic is a set of append-only partition logs; a consumer group splits the partitions and remembers an offset per partition — which gives per-partition ordering, replay from any offset and many independent readers, and makes the partition key and consumer lag the two decisions that determine whether the system behaves.
Call synchronously when the caller needs the answer now, the chain is short and consistency must be immediate; go asynchronous when the work can happen later, fan out, or arrive in bursts — and use the hybrid, a synchronous command with asynchronous consequences, for most real user-facing writes.
REST, GraphQL, RPC/gRPC, WebSockets, webhooks — which to expose to whom — and what belongs in an API gateway.
Each API style exists because a previous one hurt — REST for cacheable resources, GraphQL for clients that need many shapes, gRPC for fast typed internal calls, WebSockets for server push, webhooks for event delivery across organisations — and each carries a failure mode you inherit the moment you choose it.
One edge component that authenticates, rate-limits, routes, logs and reshapes every inbound request so the services behind it do not each reinvent those concerns — valuable exactly as long as it stays an edge and does not absorb the business logic, aggregation and orchestration that turn it back into the monolith.
Load balancing, stateless services, cache layers, CDNs, horizontal vs vertical — and scaling a system one real problem at a time.
A load balancer turns N servers into one address so capacity and availability stop depending on a single machine — and the algorithm, the health check and the balancer’s own redundancy each decide whether it helps or hurts under load.
A stateless service keeps nothing between requests that another instance would need, so any instance can serve any request and instances become disposable; the state does not vanish — it moves to Redis or the database, and that extra hop and new dependency are the price of horizontal scaling.
Caches sit at five distances from the user — browser, CDN, in-process, distributed, database buffer — each one trading freshness for latency and load, and the architecture questions are which layer answers which read, how a key expires, and what happens when thousands of requests miss at once.
A CDN puts copies of responses in hundreds of edge locations so a user in Frankfurt gets bytes from Frankfurt instead of Virginia — beating the speed of light by not crossing the ocean — and the design questions are what the cache key is, how the origin is protected from misses, and how a change reaches every edge.
Vertical scaling makes one machine bigger and adds no new failure modes until it hits the largest instance; horizontal scaling adds machines without limit and adds coordination — and stateless tiers scale out cheaply while databases pay for every rung.
Start with one server and one database and add exactly one component per measured problem — load balancer, cache and replicas, sharding decisions, CDN, queue and workers — noting at every step the symptom that forced it and the new problem it introduced, until the diagram every system-design answer draws has been earned box by box.
Background jobs, workers, idempotency under retries, and backpressure when producers outrun consumers.
Move work that does not have to finish inside the request — image processing, email, reports, AI generation, transcoding — onto a queue consumed by workers, and accept that a job which can be retried will eventually run twice.
A network gives you at-least-once delivery whether you like it or not; idempotency — the same request applied twice has the effect of once — is what turns "at least once" into the behaviour users mean by "exactly once".
When a producer emits 100,000 messages per second and the consumer handles 20,000, the queue grows by 80,000 per second and the oldest message is minutes old within minutes; backpressure is every mechanism that makes the producer feel the consumer's limit before memory, disk or latency does.
Why one transaction cannot span services: sagas, compensation, CQRS and event sourcing — and why most apps need none of them.
Once an order, a payment and an inventory reservation live in three services with three databases, no single `COMMIT` covers them; two-phase commit can make it look like one but is avoided for good reasons, so production systems use sagas, compensation and the outbox pattern instead.
A saga is a business transaction spread over several services as a sequence of local transactions, each with a compensating action; the flow either completes or is unwound step by step, passing through pending states the user can see, and driven either by an orchestrator or by a chain of events.
Command Query Responsibility Segregation splits the model that accepts writes from the model that serves reads, so each can have its own shape, storage and scaling — at the price of a projection that lags, a rebuild story, and two models to keep in agreement; most applications should stop at CQRS-lite.
Instead of storing `balance = 100`, store `AccountCreated +100`, `PaymentMade −20`, `RefundReceived +20` and derive the balance by replaying the log; you gain a complete audit trail, time travel and rebuildable projections, and pay with snapshots, event versioning, GDPR pain and a model most applications do not need.
Retry, backoff, timeouts, circuit breakers, bulkheads, rate limiting, graceful degradation, and the arithmetic of nines.
Timeouts, retries with backoff and jitter, circuit breakers, bulkheads, rate limits, fallbacks and graceful degradation each exist because of one specific failure — and each, applied without its budget, becomes a new way to turn a slow dependency into a full outage.
A circuit breaker watches the failure rate of calls to one dependency and, once it is clearly down, fails fast instead of spending a timeout on every request; Closed → Open → Half-Open → Closed is the state machine, and its thresholds decide whether it protects the system or trips on noise.
A rate limiter decides, per client, tenant or route, whether a request may proceed now; fixed windows are cheap and leak 2× at the boundary, sliding windows are exact or approximate depending on memory, and the token bucket is the default because it allows bounded bursts with O(1) state — enforced at the gateway with atomic counters in Redis and communicated with 429 + `Retry-After`.
An SLI is a measurement, an SLO is the target you set for it, an SLA is the contract with penalties; 99.9% availability is 8h 46m of downtime a year and 43 minutes a month, serial dependencies multiply their unavailability, and the error budget — the downtime you are allowed and have not yet spent — is the number that decides whether the next release ships.
Consistent hashing, service discovery, partitions and CAP without slogans.
Place nodes and keys on the same hash ring and assign each key to the first node clockwise; adding or removing a node then moves only about K/N keys instead of almost all of them, virtual nodes even out the load, and the lookup is a binary search on a sorted array — this is the DSA hash table becoming a production partitioning scheme.
When instances are created and destroyed by autoscalers, schedulers and rolling deploys, a caller cannot be configured with addresses; it asks a registry that instances join with heartbeats and leave when they stop — and the registry’s staleness window, its own availability, and who does the lookup (client, load balancer or sidecar) are the design.
A network partition is not a choice — any timeout is one — so the real decision is what a system does while it lasts: refuse writes to stay consistent, or accept writes on both sides and reconcile later; quorums (W + R > N) define the boundary precisely, and PACELC adds the everyday trade the slogan omits: latency versus consistency when nothing is broken at all.
Logs, metrics and traces; following one request across services and finding where the time went.
Three signals that answer three different questions — logs: what happened; metrics: how much and how often; traces: where did this request spend its time — each with its own cardinality and cost profile, and none of which can substitute for the other when the p99 doubles at 3 a.m.
Follow one request Browser → Gateway → Service A → Service B → Database as a tree of timed spans stitched together by a propagated trace id, so that fan-out, serial-versus-parallel calls and the 140 ms of database time hiding behind an N+1 become visible in a way no log search can reproduce.