Skip to main content

Command Palette

Search for a command to run...

How First Principles Finally Made Distributed Systems Click for Me

Updated
7 min read
How First Principles Finally Made Distributed Systems Click for Me
B
Backend engineer building reliable distributed systems in Go. Event-driven architecture, gRPC services, and systems that hold up under pressure.

I'll be honest. For a long time, I thought I understood distributed systems.

I had read the articles. I knew words like "eventual consistency," "CAP theorem," and "microservices." I could explain them in a rough way if someone asked. But if you sat me down and said — okay, now build one that actually works — I would have struggled badly.

The problem wasn't that I didn't know the tools. The problem was that I was thinking about the wrong things entirely.

This post is about the mental shift that changed how I see distributed systems. Not a deep technical dive — just the moment things started making sense to me, and the first principles that got me there.


What I Was Getting Wrong

When I first started learning about distributed systems, I approached it the same way I approached everything else in programming.

You write code. It runs. You know what happened.

result = do_something()
# result is either a value or an exception. Simple.

That mental model works perfectly when everything is on one machine. You're in control. Memory is shared. If something fails, you know it failed.

I was trying to apply that same thinking to distributed systems — and that's exactly where I was going wrong.

The moment I understood why that thinking breaks, everything else started falling into place.


The Shift: Local vs. Distributed

Here's the core difference that nobody explained to me clearly in the beginning.

In local programming, a function call either works or it doesn't. Two outcomes.

In a distributed system, a remote call has three outcomes:

1. It succeeded, and you got a response.
2. It failed, and you know it failed.
3. It might have succeeded, might have failed — you have no idea.

That third outcome — unknown — is the one I never thought about before. And it's the one that distributed systems are mostly designed around.

Once I saw that, I stopped thinking "how do I make this work?" and started thinking "how do I make this work even when I don't know what happened?"

That was the shift.


The First Principles That Helped Me

These aren't rules I memorized. They're realizations that came from trying to understand why distributed systems are designed the way they are.


The network is not reliable

I always assumed that if I send a request, it arrives. That's not true.

The network can drop requests. It can deliver them late. It can deliver them and drop the response on the way back — so the receiver processed your request but you never got the confirmation.

This broke my understanding of "just retry it." Because if you retry without thinking, you might end up doing the same thing twice. Charging a card twice. Creating the same order twice.

The network being unreliable isn't a bug. It's reality. The design has to account for it.


Failure is not exceptional — it's normal

In a system running across many machines, something is always slightly wrong somewhere. A slow node. A dropped connection. A service restarting.

Before this clicked, I thought failure was the rare case I'd handle with a try-catch and move on. In distributed systems, failure is a regular operating condition. You don't just handle it — you design for it from the start.


Partial failure is the hardest part

This one genuinely surprised me.

I expected systems to be either up or down. But real distributed systems often live somewhere in the middle. Some requests succeed, some fail. Some parts of the system are healthy, others are struggling.

Imagine you're placing an order. Payment goes through. Inventory gets reserved. But the notification service times out.

Did the order succeed? Technically yes. But what do you do about the notification? Roll everything back? Let it go? Retry just that part?

Partial failure means you can't always answer "did it work?" with a simple yes or no. The system has to be built knowing that some things will fail while others succeed, and that's a completely different design problem than "what if everything fails."


Time is not consistent across machines

This one confused me at first, but it makes sense once you sit with it.

Every machine has its own clock, and clocks drift. Two machines might disagree on what time it is by a small amount. In most cases that doesn't matter. But when you're trying to figure out which event happened first — which write won, which request came in earlier — a few milliseconds of drift can give you the wrong answer.

This is why you can't just use timestamps to order events across distributed systems. Two events with nearly identical timestamps might have actually happened in either order, depending on which machine you ask.

I used to think ordering events was simple. It's not.


Retrying is necessary but dangerous without idempotency

Since the network is unreliable, retrying failed requests is necessary. But retrying without thinking creates new problems.

If you charge a card, the request times out, and you retry — did you just charge twice?

The solution is idempotency. An operation is idempotent if doing it multiple times gives the same result as doing it once. You design your operations so that retrying is safe.

// Client sends a unique key with the request
POST /charge
{
  "amount": 50,
  "idempotency_key": "unique-id-abc123"
}

// If the server sees this key again, it returns
// the original result instead of running again.

This pattern is why many payment APIs work this way. It's not a nice-to-have — it's what makes retrying safe.


Systems are always in transition

I used to imagine a deployed system as a stable, consistent thing. Everything running the same version. Everything in sync.

That's almost never true.

During a deployment, some servers are running old code, some are running new. A database migration might be halfway done. A cache might have data from before a schema change.

The distributed system is always somewhere between states. Which means every change you make has to work while the old version is still running alongside it. You can't flip a switch and have everything consistent at once.


How These Principles Connect to Real Patterns

Once these principles settled in my head, the patterns I kept reading about stopped feeling like buzzwords.

Why event-driven systems? Because if Service A calls Service B directly and B is down, A breaks too. But if A just emits an event and B consumes it whenever it's ready — partial failure doesn't cascade. They're decoupled.

Why circuit breakers? Because if a service is slow or failing, you don't want to keep waiting on it and tying up resources. A circuit breaker detects sustained failure and stops making calls for a while — failing fast instead of failing slowly.

Why the Saga pattern? Because you can't do a reliable transaction across multiple services the same way you do it in a single database. Saga breaks it into steps, and each step has a compensating action if something later fails.

Order created → reserve inventory → charge payment → send notification

If payment fails:
→ release inventory → cancel order

It's not simple. But it's honest about the reality of partial failure.


What Actually Changed for Me

I stopped asking "how do I make this work?" and started asking "how do I make this work when parts of it fail, when messages get lost, and when I don't always know what happened?"

That question change is everything.

Distributed systems aren't hard because the code is complicated. They're hard because the assumptions underneath normal programming don't hold. Once I stopped fighting that and started designing around it, everything made more sense.

I'm still learning a lot. But these principles gave me a foundation that actually feels solid — not just a list of tools to memorize.

If you're in the same place I was — knowing the terms but not really feeling it — I hope this helps. Start with the mental shift. The rest builds from there.

4 views