Module 06 · Architecture · All tracks
Software Architecture
Architecture is the art of making the expensive-to-change decisions well. This module is about reasoning through structural tradeoffs — the thing senior interviews are really testing.
By the end you'll be able to explain, with conviction:
- Monolith vs microservices as a tradeoff, not a trend.
- Layered, clean, and event-driven styles and when each fits.
- Caching, load balancing, queues, and the resilience patterns that keep systems up.
1Monolith vs microservices vs modular monolith
The most over-asked architecture question — and the one most people answer with dogma instead of judgment.
A monolith is one deployable unit. It's simpler to build, test, deploy, and debug — no network between components, no distributed-systems tax. Its weakness shows at scale of team and codebase: everything ships together, a bug anywhere can take down everything, and you can't scale one hot component independently.
Microservices split the system into small, independently deployable services owned by separate teams. The benefits are independent deployment, independent scaling, and fault isolation. But you pay a heavy tax: network latency and failures, distributed transactions, eventual consistency, and a whole operational burden (service discovery, tracing, orchestration). Most teams adopt them too early.
The sophisticated middle is the modular monolith: one deployable, but with strict internal module boundaries. You get monolith simplicity now and a clean seam to extract services later if scale demands it. The senior position is almost always "start with a modular monolith; split out services only when a specific scaling or team-autonomy pressure justifies the distributed-systems cost."
Interview angle
"Microservices solve an organisational and scaling problem, not a code-quality one — and they cost you a distributed system. I'd start with a modular monolith for clean boundaries without the network tax, and extract services only where a real scaling or team-ownership pressure appears."
2Layered & MVC
The classic layered architecture separates concerns into horizontal tiers: presentation → business logic → data access → database. Each layer talks only to the one below, so changes stay contained — you can swap the database layer without touching business logic. It's the default for a reason: it's simple and everyone understands it.
MVC (Model-View-Controller) is the same instinct applied to interactive apps: the Model holds data and rules, the View renders, the Controller mediates input. The point of both is the same as encapsulation at the class level (Module 01) — separate concerns so each part has one job and one reason to change. The trap is the "fat controller" or "anaemic model" where logic leaks into the wrong layer.
3Event-driven architecture
Instead of services calling each other directly (synchronous, tightly coupled), event-driven systems communicate by emitting and reacting to events. An OrderPlaced event is published; inventory, email, and analytics services each react independently — and the publisher has no idea they exist. It's the Observer pattern (Module 01) at system scale.
The payoff is loose coupling and extensibility: add a new reaction without modifying the producer. The cost is that flow becomes harder to trace and you inherit eventual consistency — the system is correct eventually, not instantly. Great for decoupling and scale; harder to debug end-to-end.
Interview angle
"Event-driven architecture trades synchronous simplicity for loose coupling — producers emit events without knowing the consumers, so I can add reactions freely. The cost is harder tracing and eventual consistency, so I use it where decoupling and scale matter more than instant, traceable flow."
4Hexagonal / Clean architecture
These styles (Ports & Adapters / Clean / Onion — same core idea) push one principle hard: your business logic should not depend on infrastructure. The domain sits at the centre, knowing nothing about databases, web frameworks, or message brokers. Those external concerns plug in at the edges through interfaces (ports), implemented by adapters.
This is Dependency Inversion (Module 01's "D" in SOLID) scaled to a whole system. The dependencies point inward — infrastructure depends on the domain, never the reverse. The payoff: you can test business logic with no database, and swap Postgres for Mongo or REST for gRPC by writing a new adapter, leaving the core untouched. It's more upfront structure, justified when the domain is rich and long-lived.
Go deeper
The litmus test for clean architecture: can you unit-test your core business rules without spinning up a database or web server? If yes, your dependencies point inward correctly. If you need the whole stack to test a pricing rule, infrastructure has leaked into the core.
5DDD & bounded context
Domain-Driven Design is about modelling software around the real business domain, using a ubiquitous language shared by engineers and domain experts — the code uses the same words the business does. Its most quoted concept is the bounded context: a boundary within which a model and its terms are consistent.
The insight that makes it click: the word "customer" means different things to Sales, Billing, and Support — so forcing one giant shared "Customer" model creates a tangled mess. Bounded contexts let each part of the business have its own coherent model. This is also the soundest way to decide microservice boundaries: draw services along bounded contexts, not arbitrary technical lines.
Interview angle
"DDD models software in the business's own language, and its bounded contexts give you a principled way to split a system — each context owns a coherent model. When I draw microservice boundaries, I draw them along bounded contexts, not technical layers."
6CQRS & Event Sourcing
Awareness-level, but worth being able to define cleanly. CQRS (Command Query Responsibility Segregation) separates the write model from the read model — they can use different schemas or even different databases, each optimised for its job. It shines when read and write loads are wildly asymmetric, at the cost of added complexity.
Event Sourcing stores state as an immutable log of events rather than just the current snapshot. Current state is derived by replaying events. You gain a perfect audit trail and the ability to reconstruct any past state — invaluable for finance — but querying current state is harder and the model is more complex. The two are often paired but independent. Know them; don't reach for them casually.
7Caching strategies
Caching trades freshness for speed by keeping hot data closer to the consumer. The strategies you should name:
- Cache-aside (lazy) — app checks the cache; on a miss it loads from the DB and populates the cache. The most common pattern.
- Write-through — writes go to cache and DB together; reads are always fresh, writes a bit slower.
- Write-back — write to cache, flush to DB later; fast writes, risk of loss on failure.
The two hard problems are invalidation (how do you know cached data is stale? — "there are only two hard things in CS...") and eviction (what to drop when full — usually LRU). And mention TTLs. Demonstrating you know caching introduces a consistency problem, not just speed, is the senior signal.
Common trap
"Just add a cache" without addressing invalidation is a junior answer. The whole difficulty of caching is keeping it correct as the source changes — lead with that, not with the speed win.
8Load balancing — L4 vs L7
A load balancer spreads traffic across multiple servers so no single one is overwhelmed — the foundation of horizontal scaling and high availability. It also health-checks instances and routes around dead ones.
The distinction interviewers like: L4 (transport layer) balances on IP and port — fast and protocol-agnostic, but it can't see request content. L7 (application layer) understands HTTP, so it can route by URL path, headers, or cookies (e.g. /api to one pool, /images to another), terminate TLS, and do content-based routing — richer, slightly slower. Common algorithms: round-robin, least-connections, IP-hash for stickiness.
Interview angle
"L4 balancing works on IP/port — fast and dumb; L7 understands HTTP so it can route by path or header and terminate TLS. I'd use L7 when I need content-based routing, L4 when I just need raw throughput."
9Message queues — Kafka vs RabbitMQ vs SQS
Queues decouple producers from consumers and absorb load spikes — work is buffered and processed at the consumer's pace, so a traffic surge fills the queue instead of crashing the service. They also enable async processing and retries.
| Kafka | RabbitMQ | SQS | |
|---|---|---|---|
| Model | Distributed log | Message broker | Managed queue (AWS) |
| Strength | High throughput, replay, streaming | Flexible routing, low latency | Zero-ops, simple, scalable |
| Best for | Event streaming, analytics pipelines | Complex task routing | Simple decoupling on AWS |
The key conceptual split: Kafka is a durable, replayable log (consumers track their own position and can re-read history), whereas a traditional broker like RabbitMQ typically deletes a message once acknowledged. That "log vs queue" distinction is the one to land.
10Resilience — circuit breaker, retry, Saga
In a distributed system, failure is normal, so you design for it. Three patterns recur:
- Retry with backoff — retry a transient failure, but with exponential backoff and jitter so you don't hammer a struggling service. Pair with idempotency (Module 04) so retries are safe.
- Circuit breaker — after repeated failures, "trip" and stop calling a failing dependency for a while, failing fast instead of piling up. This stops one sick service from cascading into a system-wide outage.
- Saga — manages a transaction spanning multiple services without a distributed lock: a sequence of local transactions, each with a compensating action to undo it if a later step fails. It's how you keep consistency across microservices.
Interview angle
"I assume failure is normal: retries with exponential backoff for transient errors, a circuit breaker so a failing dependency fails fast instead of cascading, and the Saga pattern with compensating actions for transactions that span services. Resilience is designed in, not bolted on."
Recap — what you can now teach
- Microservices solve a scaling/org problem at a distributed-systems cost — start with a modular monolith.
- Layered/MVC and clean architecture both separate concerns; clean pushes dependencies inward, away from infrastructure.
- Event-driven design buys loose coupling at the cost of traceability and eventual consistency.
- Bounded contexts are the principled way to draw service boundaries.
- Caching's hard part is invalidation, not speed; L4 vs L7 balancing is dumb-fast vs content-aware.
- Kafka is a replayable log; resilience = retry+backoff, circuit breaker, Saga.
Self-check
Say each answer out loud before revealing it.
What problem do microservices actually solve, and what do they cost?
They solve independent scaling and team autonomy — at the cost of becoming a distributed system (network failures, eventual consistency, operational complexity).
In clean architecture, which way do dependencies point?
Inward — infrastructure depends on the domain core, never the reverse, so business logic is testable without a database or framework.
What's the hardest part of caching?
Invalidation — knowing when cached data is stale and keeping it consistent with the source as it changes. Caching adds a consistency problem, not just speed.
How does Kafka differ conceptually from RabbitMQ?
Kafka is a durable, replayable log — consumers track their position and can re-read history; RabbitMQ is a broker that typically removes a message once it's acknowledged.
What does a circuit breaker prevent?
Cascading failure — after repeated errors it trips and fails fast instead of continuing to call a failing dependency and piling up requests across the system.