DEV Community

Mohammad-Idrees
Mohammad-Idrees

Posted on

Designing Systems by Questioning from First Principles

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:

  1. What exists?
  2. What changes?
  3. What must never break?
  4. What can happen at the same time?
  5. 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')
)
Enter fullscreen mode Exit fullscreen mode

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:

  1. What are the entities?
  2. Which ones change over time?
  3. What is identity vs state?
  4. Which events are independent / async?
  5. What invariants must never break?
  6. 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)