System Design
Monolith vs microservices, layered and hexagonal architecture, caching, messaging, and scalability.
On this page
Writing a class is one skill; designing a whole system is another. System design is about the big-picture structure: how components fit together, how an app scales to millions of users, and the trade-offs behind those decisions. This is where senior engineers earn their title.
Architecture: how you organize an app
Layered architecture
The classic structure splits an app into horizontal layers, each with one responsibility:
Controller (handles HTTP requests/responses)
↓
Service (business logic)
↓
Repository (data access)
↓
DatabaseThe waiter (controller) takes your order, the kitchen (service) prepares it, and the pantry (repository) supplies ingredients. Each layer talks only to the one below it. You can renovate the dining room without touching the pantry - layers keep concerns separate and changes contained.
Hexagonal (ports & adapters)
A more advanced style puts your business logic at the center, with the outside world (databases, APIs, UIs) plugged in through adapters. Your core logic doesn't depend on any specific database or framework - you could swap them without touching it. This is Dependency Inversion applied at the architecture level.
Monolith vs. microservices
A monolith is one big shop selling everything - simple to run, but if it closes, everything stops, and remodeling one section disrupts the whole store. Microservices are a food court of independent stalls - each team runs its own stall (service), scales and updates it alone, but now you must coordinate many stalls and their communication.
| Monolith | Microservices | |
|---|---|---|
| Structure | One deployable app | Many small services |
| Simplicity | Simple to build & deploy | Complex to operate |
| Scaling | Scale the whole thing | Scale services independently |
| Best for | Most apps, especially early | Large orgs, independent teams |
Start with a monolith
Microservices bring real operational complexity - networking, distributed data, monitoring. For most projects, a well-structured monolith is the right start; split into services only when a genuine need (team scaling, independent deployment) appears. Don't adopt microservices for their own sake.
Scaling: handling more load
- Vertical scaling - a bigger machine (more CPU/RAM). Simple, but has a ceiling.
- Horizontal scaling - more machines behind a load balancer that spreads requests. Scales far, but requires your app to be stateless so any server can handle any request.
Caching - don't recompute what you can remember
A cache stores results of expensive operations so repeat requests are instant.
Rather than walking to the shop (database) every time you're hungry, you keep snacks on your desk (cache) for instant access. You only make the trip when the desk is empty. Caching trades a little memory for a lot of speed - but you must decide when the snacks go stale.
Cache invalidation is hard
"There are only two hard things in computer science: cache invalidation and naming things." A cache that serves outdated data causes subtle bugs. Decide carefully when cached data expires or is refreshed.
Messaging - decoupling with queues
Instead of services calling each other directly, they can communicate through a message queue (like Kafka or RabbitMQ). A producer drops a message; consumers process it when ready. This decouples services, smooths spikes, and improves resilience - if a consumer is down, messages wait.
Reliability & the 12-factor mindset
Well-designed systems follow principles like the 12-factor app: store config in the environment, treat backing services as attached resources, keep processes stateless, and log to standard output. These make apps portable, scalable, and cloud-friendly.
Quick check
For a new project with a small team, which architecture is usually the wisest starting point?
Key takeaways
- Layered architecture separates controller, service, and repository responsibilities.
- Hexagonal architecture keeps business logic central and plugs in databases/UIs via adapters.
- Monoliths are simpler; microservices scale teams and services independently at high operational cost.
- Scale vertically (bigger machine) or horizontally (more machines + load balancer + stateless apps).
- Caching trades memory for speed - but invalidation is genuinely hard.
- Message queues decouple services and add resilience; 12-factor principles keep apps cloud-friendly.
Systems must be observable to run reliably. Next: logging and observability.