System design isn't about drawing more boxes. It's about knowing when the next box becomes necessary.
Most system-design diagrams eventually look something like this:
Load balancer. Redis. Kafka. Replicas. CDN. Microservices.
It certainly looks like system design.
But remove the labels and ask one question:
Why does each box exist?
That's where things become interesting.
If the answer is:
"Because scalable architectures use Redis."
or:
"Kafka is good for microservices."
or:
"We need Kubernetes because this is production."
then we aren't really designing a system.
We're assembling technologies.
Modern system design is much more about reasoning through change:
And that is what this series is about.
Welcome to System Design from Developer to Architect.
Start With Almost Nothing
Imagine we're building a service-booking platform.
Customers need to:
- find a professional,
- check availability,
- reserve a time slot,
- pay,
- receive confirmation.
There are ten users.
What architecture do we need?
Probably this:
That's it.
No Redis.
No Kafka.
No Kubernetes.
No microservices.
And that's not an amateur architecture.
For the requirements we currently know, it may be exactly the right architecture.
Good architecture is not the architecture with the most components. It's the architecture with the least unnecessary complexity.
Then Traffic Arrives
Our product starts getting traction.
The server that happily handled a few hundred requests is now struggling.
Now we have a problem.
And importantly:
we had the problem before we introduced the solution.
That's how I like to approach system design.
Instead of:
Technology → Find somewhere to use it
we want:
Problem → Constraints → Options → Decision
Our first option might simply be a larger server.
4 CPU → 16 CPU
8 GB → 64 GB RAM
Vertical scaling.
Simple.
Often effective.
But eventually we may want to run multiple application instances.
And the moment we do that, our architecture changes.
The Second Server Changes Everything
We go from:
Why did the load balancer appear?
Not because:
"Architectures should have load balancers."
It appeared because we now have multiple application instances and need to distribute requests between them.
One box.
One reason.
But our solution immediately creates another problem.
Wait... Where Did My Session Go?
Imagine authentication sessions are stored in application memory.
The user logs in:
Login
│
▼
App 1
Session stored in App 1
Next request:
GET /bookings
│
▼
App 2
"Who are you?"
The load balancer did its job perfectly.
Our architecture didn't.
Horizontal scaling has exposed a state problem.
Now we have architectural choices.
We could make the application stateless.
Or introduce shared session storage.
For example:
Redis finally enters our architecture.
But notice the sequence:
More traffic
↓
Multiple servers
↓
Requests move between servers
↓
Local session state becomes problematic
↓
Need shared/distributed state
↓
Redis becomes one possible solution
That's very different from starting the architecture with Redis because "Redis is fast."
The Bottleneck Moves
We add more application servers.
Everything looks scalable.
Until the dashboard says:
API CPU 35% ✓
API memory 42% ✓
DB CPU 94% ⚠
DB connections 97% ⚠
Query latency 850ms ⚠
Adding another API instance isn't going to save us.
Our bottleneck moved.
This is an important mental model:
Scaling doesn't eliminate bottlenecks. It moves them.
Maybe the problem is missing indexes.
Maybe we're making unnecessary queries.
Maybe one expensive query dominates database time.
Maybe we need caching.
Maybe reads need replicas.
Maybe our data model is wrong.
The architecture should not answer those questions before the evidence does.
Enter the Cache
Suppose profiling reveals something interesting.
Thousands of requests repeatedly fetch information that barely changes:
GET /services
GET /categories
GET /professionals/123
Every request goes to the database.
Now caching has a concrete job.
Great.
Latency drops.
Database traffic drops.
Everyone celebrates.
Until this happens.
Fast and Wrong Is Still Wrong
A professional has one remaining slot:
10:00 AM → AVAILABLE
The database says:
10:00 AM → AVAILABLE
The cache says:
10:00 AM → AVAILABLE
Customer A books it.
The database becomes:
10:00 AM → BOOKED
But for a short period, the cache still says:
10:00 AM → AVAILABLE
Customer B sees the stale value.
Our system is faster.
But it may now be showing incorrect availability.
This is the other half of architecture that diagrams often hide.
Every solution comes with a bill.
Cache
│
├── + Lower latency
├── + Lower DB load
│
├── - Stale data
├── - Invalidation
├── - Stampedes
└── - Additional failure mode
The interesting question isn't "Should we use Redis?"
It's:
"Which data are we willing to serve stale, and for how long?"
That is a much more useful architecture discussion.
Then Someone Says, "Let's Add Kafka"
Eventually, our booking workflow grows.
When a booking succeeds we need to:
Create booking
│
├── Send email
├── Send push notification
├── Update analytics
├── Award loyalty points
└── Notify professional
Should the customer wait while every one of those operations completes?
Probably not.
Now asynchronous processing becomes attractive.
Kafka, RabbitMQ, SQS or another messaging system might now solve a real problem.
But we just bought ourselves another collection of problems.
What if the same event arrives twice?
What if events arrive out of order?
What if the consumer crashes?
What if processing fails repeatedly?
What if the booking commits to the database but publishing BookingCreated fails?
Database COMMIT ✓
Event publish ✗
That's a tiny diagram containing a very large distributed-systems problem.
We'll get to it later in this series.
Architecture Is a Sequence, Not a Snapshot
This is why I think one giant "final architecture" diagram is often a poor way to learn system design.
It shows where the system ended up.
It doesn't explain how it got there.
A better way is to watch the architecture evolve.
The final diagram is not the lesson.
The transitions are.
Use This Framework for Every Architecture Decision
For every new box we introduce in this series, we'll ask five questions.
1 — What changed?
Traffic?
Data volume?
Availability requirement?
Latency requirement?
Business workflow?
2 — What broke?
CPU?
Memory?
Database?
Network?
Consistency?
Reliability?
Developer velocity?
3 — What options do we have?
There should usually be more than one.
4 — Why are we choosing this option?
This is where trade-offs matter.
5 — What new failure modes did we introduce?
This is the question people skip.
And it's often the most important one.
Every Box Has a Cost
Here's a useful way to look at common architecture components.
| We add... | Because we need... | But now we must think about... |
|---|---|---|
| Load Balancer | Horizontal scaling | Health checks, routing, failure detection |
| Redis | Lower latency / shared state | Staleness, eviction, invalidation |
| Read Replicas | More read capacity | Replication lag |
| CDN | Lower global latency | Cache invalidation |
| Message Broker | Async workflows | Duplicates, ordering, retries |
| Microservices | Independent boundaries | Network failures, distributed transactions |
| Retries | Resilience to transient failure | Duplicates, retry storms |
| Sharding | Larger data scale | Routing, rebalancing, cross-shard operations |
Architecture becomes much easier to reason about when we stop seeing components as features and start seeing them as trade-offs.
Think About the Failure Path
Developers naturally focus on this:
Request
↓
Process
↓
Success
Architectural thinking requires another diagram.
Request
│
├── Success
│
├── Timeout
│
├── Partial success
│
├── Dependency unavailable
│
├── Duplicate request
│
└── Concurrent request
Consider our booking platform.
Two customers click the same slot at almost exactly the same time.
Who gets the slot?
Now we're talking about concurrency.
Maybe transactions.
Maybe optimistic locking.
Maybe pessimistic locking.
Maybe a database constraint.
Maybe temporary reservations.
The correct answer depends on our requirements.
Now Make Payment Fail
Our booking workflow becomes:
Reserve Slot
│
▼
Create Booking
│
▼
Charge Payment
│
▼
Confirm Booking
Happy path?
Easy.
Now:
Reserve Slot ✓
Create Booking ✓
Charge Payment ✓
Confirm Booking ✗
The customer's card has been charged.
But their booking isn't confirmed.
What now?
Retry?
Refund?
Compensate?
Reconcile later?
And what happens if the payment API timed out?
If we blindly retry, we might charge the customer twice.
Suddenly a seemingly simple requirement—
"Let customers pay."
—has led us to idempotency.
This is what makes system design interesting.
Stop Asking "What Technology Should I Use?"
Try replacing technology questions with engineering questions.
Instead of:
Should I use Kafka?
Ask:
Do these operations need to happen synchronously?
Instead of:
Should I use Redis?
Ask:
Which reads are expensive, repetitive and safe to cache?
Instead of:
Should I use microservices?
Ask:
Which domains need independent ownership, deployment or scaling?
Instead of:
Should I use NoSQL?
Ask:
What are my access patterns, consistency requirements and data relationships?
Instead of:
Should I use Kubernetes?
Ask:
What deployment and orchestration problems do I actually have?
The quality of the architecture usually improves when the quality of the question improves.
The Architect's Loop
The mental model we'll use throughout this series is simple:
You don't finish architecture.
You continuously make better decisions as the system changes.
What We're Going to Build in This Series
We're going to start here:
User
│
▼
Server
│
▼
Database
And gradually evolve toward something closer to:
But we're not going to jump directly there.
We'll earn every box.
We'll encounter the problem first.
Then introduce the concept.
Then look at the solution.
Then deliberately try to break it.
Where We're Going
The first part of this series will build the foundations:
Single Server
↓
Database
↓
Vertical Scaling
↓
Horizontal Scaling
↓
Load Balancing
↓
Caching
↓
API Design
↓
Communication Protocols
↓
Authentication
↓
Authorization
↓
Security
Then we'll move into the problems that make production systems interesting:
Concurrency
↓
Transactions
↓
Idempotency
↓
Retries
↓
Event Delivery
↓
Distributed Transactions
↓
Saga
↓
Transactional Outbox
↓
Caching & Consistency
↓
Observability
↓
Resilience
And eventually we'll bring those ideas together in complete system-design case studies.
One Rule Before We Continue
When you see an architecture diagram, don't start by asking:
What technologies are they using?
Start with:
What problem forced this box to exist?
Then ask:
What would happen if I removed it?
And finally:
What new failure modes did adding it create?
If you can answer those three questions for every important component, you're no longer memorizing architecture diagrams.
You're reasoning about systems.
And that's the skill we're going to build.
Up Next: We Add Users Until Something Breaks
We begin with the smallest architecture possible:
User → Server → Database
Then we'll increase the traffic.
1 user
↓
100 users
↓
10,000 users
↓
100,000 users
↓
1,000,000 users
At each stage, we'll ask the same question:
What breaks next?
And we'll change the architecture only when we have a reason to.
Next → Part 1
How to Scale a Backend From 1 User to 1 Million Users
This is **Part 0* of System Design from Developer to Architect — a practical series about scalability, databases, APIs, distributed systems and the engineering decisions behind production architecture.*
Suggested DEV.to tags: #systemdesign #architecture #backend #programming














Top comments (0)