DEV Community

Cover image for How to Think About System Design Without Just Drawing Boxes
Sanu Khan
Sanu Khan

Posted on

How to Think About System Design Without Just Drawing Boxes

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
Enter fullscreen mode Exit fullscreen mode

we want:

Problem → Constraints → Options → Decision
Enter fullscreen mode Exit fullscreen mode

Our first option might simply be a larger server.

4 CPU  →  16 CPU
8 GB   →  64 GB RAM
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next request:

GET /bookings
      │
      ▼
    App 2

"Who are you?"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ⚠
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The database says:

10:00 AM → AVAILABLE
Enter fullscreen mode Exit fullscreen mode

The cache says:

10:00 AM → AVAILABLE
Enter fullscreen mode Exit fullscreen mode

Customer A books it.

The database becomes:

10:00 AM → BOOKED
Enter fullscreen mode Exit fullscreen mode

But for a short period, the cache still says:

10:00 AM → AVAILABLE
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen 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
Enter fullscreen mode Exit fullscreen mode

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     ✗
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Architectural thinking requires another diagram.

Request
   │
   ├── Success
   │
   ├── Timeout
   │
   ├── Partial success
   │
   ├── Dependency unavailable
   │
   ├── Duplicate request
   │
   └── Concurrent request
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Happy path?

Easy.

Now:

Reserve Slot       ✓

Create Booking     ✓

Charge Payment     ✓

Confirm Booking    ✗
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then we'll increase the traffic.

1 user
   ↓
100 users
   ↓
10,000 users
   ↓
100,000 users
   ↓
1,000,000 users
Enter fullscreen mode Exit fullscreen mode

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)