Why this blog exists
Most system design explanations jump straight to:
- tables
- services
- Kafka
- microservices
- “best practices”
That’s intimidating — and worse, it hides how good designs are actually created.
Strong engineers don’t start with solutions.
They start with questions.
This blog teaches you:
- How to think, not what to memorize
- Which questions to ask, in which order
- How to reason from first principles
- How interviewers expect you to reason, even if they never say it
No prior knowledge required.
What is “first principles” thinking?
First principles thinking means:
Breaking a problem down to what must be true, before deciding how to implement anything.
Instead of:
“I’ll use X because everyone uses X”
You ask:
“What does this system fundamentally need to do?”
This applies to any problem:
- backend
- frontend
- infra
- data
- even non-technical problems
The Core Idea: Design is a Questioning Process
Good design emerges from progressively sharper questions.
Think of it like peeling an onion:
- What exists?
- What changes?
- What must never break?
- What can happen at the same time?
- What happens if things arrive late, twice, or out of order?
Each layer removes ambiguity.
The 6 First-Principles Questions (Problem-Agnostic)
These six questions work for any system design problem.
1️⃣ What are the things that exist?
Before tables, before APIs — ask:
“What are the nouns in this system?”
Examples:
- user
- order
- referral
- payment
- message
These are entities.
💡 Rule:
If you can point at it or name it, it’s probably an entity.
Do not think about storage yet.
2️⃣ Which of these change over time?
Now ask:
“Which entities evolve?”
Examples:
- order → created → paid → shipped
- referral → sent → joined → failed
This introduces state.
💡 Rule:
If something changes, you must model how it changes.
This is where many designs fail — they ignore time.
3️⃣ Which data is identity vs state?
This is a critical mental separation.
Ask:
“Which fields define what this thing is?”
“Which fields define where it is in a process?”
Identity
- IDs
- relationships
- who is involved
- usually set once
State
- status
- progress
- lifecycle
- changes often
💡 Rule:
Identity answers “what is it?”
State answers “what is happening to it?”
You don’t need separate tables yet — just separate thinking.
4️⃣ What events can happen independently?
Now introduce time and reality.
Ask:
“Can these things happen at the same time?”
“Can they arrive out of order?”
“Can they be retried?”
Examples:
- install event
- signup event
- payment confirmation
Even in one service, these can be async.
💡 Rule:
Async is about timing, not microservices.
5️⃣ What must never be allowed to happen?
These are invariants.
Ask:
“What states are illegal?”
“What combinations should never exist?”
Examples:
- reward given twice
- joined without a user
- paid order without payment record
💡 Rule:
Invariants are stronger than code — enforce them in data models when possible.
6️⃣ Where can the system safely lose information?
This is subtle and very important.
Ask:
“If two things race, is it okay if one wins?”
“Do I need the full history, or only the outcome?”
Examples:
- Final status (joined vs not joined) → overwrite OK
- Money ledger → overwrite NOT OK
💡 Rule:
If overwriting loses truth, you need append-only data.
Case Study: Referral System (Simplified)
Let’s apply the questions to a concrete example.
Problem (simplified)
Users invite friends.
Friend either:
- joins using the referral
- or doesn’t
Step 1: Entities
- Referral
- User
Step 2: What changes?
Referral has a lifecycle.
Step 3: Identity vs State
Identity
- referrer_user_id
- referee_user_id (once joined)
State
- invite-sent
- joined
- not-joined
Step 4: Async reality
Events:
- invite sent
- signup
- code applied / missed
These can race.
Step 5: Invariants
- joined ⇒ referee_user_id exists
- not-joined ⇒ referee_user_id is null
Step 6: Can we lose intermediate info?
Yes.
We only care about:
- joined
- not joined
We don’t need:
- install timestamp
- intermediate states
Resulting Design
A single table is enough:
referrals (
referral_id,
referrer_user_id,
referee_user_id NULL,
status ENUM('INVITE_SENT', 'JOINED', 'NOT_JOINED')
)
Why this works:
- states are terminal
- outcomes are mutually exclusive
- last write wins is acceptable
No over-engineering.
Why This Thinking Wins Interviews
Interviewers are not testing:
- syntax
- frameworks
- memorized architectures
They are testing:
- reasoning
- clarity
- trade-off awareness
If you explain:
“I chose X because these states are terminal and overwrites are safe”
You sound senior — even if the solution is simple.
Common Mistakes Junior Engineers Make
❌ Starting with databases
❌ Overusing “microservices”
❌ Adding Kafka without events
❌ Designing for scale without defining scale
❌ Not questioning requirements
The One-Page Interview Checklist
You can memorize this.
Before Designing Anything, Ask:
- What are the entities?
- Which ones change over time?
- What is identity vs state?
- Which events are independent / async?
- What invariants must never break?
- Can overwrites lose truth?
If you answer these clearly, the design almost writes itself.
Final Thought
Great system design is not about being clever.
It’s about being:
- clear
- deliberate
- honest about trade-offs
If you learn to ask better questions, you will:
- design better systems
- perform better in interviews
- grow faster as an engineer
And most importantly — you’ll know why your design works.
Top comments (0)