Every backend developer has shipped some version of this: an order processing system for an e-commerce platform.
A customer places an order, and everything seems straightforward. Your application receives a webhook, validates the request, stores the order in a database, publishes an event to a queue, updates inventory, triggers a notification, and sends data to an analytics service.
During development, everything behaves exactly as expected. Your tests pass, staging looks healthy, and deployment goes smoothly.
Three days after deployment, the first support ticket arrives.
Some customers receive duplicate notifications. A few orders appear in analytics but not in the inventory system. Occasionally, the same webhook seems to be processed twice, even though your API only received a single request.
None of these issues was caused by complicated algorithms. They happened because your application stopped being a single program and became a distributed system.
The challenges in distributed systems are rarely about writing more code. They're about dealing with uncertainty. Networks fail. Messages are delayed. Services crash. Requests are retried. Machines disagree on time. Events don't always arrive in the order you expect.
If you've built applications that consume webhooks, publish messages to queues, call external APIs, or communicate with microservices, you've already worked with distributed systems, even if you never thought of them that way.
The moment your application depends on another service over a network, you're no longer just writing code. You're designing a distributed system.
What Exactly Is a Distributed System?
We often imagine distributed systems as something only companies like Google or Netflix build. In reality, most backend developers work with one every day.
If your application consumes webhooks, publishes messages, calls payment gateways, or talks to another microservice, you're already building a distributed system. This applies regardless of the programming language, cloud provider, or architecture you're using.
Unlike a monolithic application, where everything runs inside a single process, distributed systems split responsibilities across multiple services. Each service owns a specific piece of functionality and communicates with others over a network.
Customer
│
▼
API Gateway / ALB
│
▼
Backend Service
│
▼
Store Order in Database
│
▼
Amazon SQS Queue
│
▼
Worker / Consumer
┌──────────────┬──────────────┐
▼ ▼ ▼
Notification Inventory Analytics
Service Service Service
│
▼
Email / SMS ProviderEvery arrow in the architecture diagram above represents a network call. Unlike a function call inside the same application, network communication introduces uncertainty. A request might take a few milliseconds or several seconds. A service might be temporarily unavailable. A response might never arrive. A message might be delivered twice because the sender retried after a timeout.
The complexity doesn't come from having multiple services. It comes from the fact that these services operate independently, fail independently, and have no shared view of the system at any given moment.
In a monolithic application, a function call either succeeds or throws an exception. In a distributed system, there are many more possibilities. Did the request fail, or was the response lost? Did the downstream service process the event before crashing? Should you retry, or will that create duplicate work?
These are the kinds of questions that make distributed systems different in kind from traditional applications.
Seven Distributed Systems Challenges That Break Production
The first time production behaved differently from my local environment, I assumed I'd introduced a bug.
We write a function, test it, deploy it, and expect it to behave the same way in production. Then I started working on systems that processed asynchronous events. Some requests would succeed but never return a response. A webhook would occasionally be delivered twice. A worker would crash midway through processing a message.
Initially, each of these incidents felt like an isolated bug. I'd fix one issue, only to discover another with a completely different symptom.
Over time, I realized something important: the problem wasn't my business logic. My assumptions were.
I took the network for granted. I expected every request to be processed exactly once. I treated services as if they always agreed on the current state. I assumed time flowed the same way everywhere.
None of those assumptions holds true in a distributed system.
These aren't new ideas. Peter Deutsch articulated them as the fallacies of distributed computing back in 1994. But reading about them and experiencing them in production are two very different things.
Let's walk through the seven most common distributed systems challenges.
The Seven Challenges at a Glance
| Challenge | What you actually see in production | What it forces you to build |
| 1. The Network Is Unreliable | A request times out. You can't tell if it never arrived, succeeded with a lost response, or crashed mid-processing. | Timeouts, retries with exponential backoff |
| 2. Partial Failures Are Silent | One service is down. Dashboards stay green, health checks pass, and only part of the workflow is broken. | Per-service health checks, end-to-end observability |
| 3. Messages Can Be Delivered More Than Once | The same webhook is processed twice because the sender retried after a lost acknowledgment. | Idempotency keys, deduplication |
| 4. Messages Don't Always Arrive in Order | "Order Updated" lands before "Order Created." | Versioning, sequence numbers, event replay |
| 5. Time Isn't as Trustworthy as You Think | Two servers' clocks disagree by milliseconds, so "latest timestamp wins" picks the wrong write. | Logical clocks, explicit conflict resolution |
| 6. State Lives Everywhere | One service says "paid," another still says "processing." Both are correct. | Eventual consistency as a deliberate trade-off |
| 7. Failure Is the Default | Services restart, workers crash, APIs error - routinely, not exceptionally. | Dead-letter queues, circuit breakers, graceful degradation |
1. The Network Is Unreliable
In a distributed system, every network call introduces uncertainty: packets get delayed, connections time out, and responses can be lost even when the receiving service processes the request successfully.
One of the hardest lessons I learned was that the network is not something you can control.
When I first started building backend systems, I treated a network call almost like a function call. I assumed that if I sent a request, I'd either receive a successful response or an error.
Production quickly proved me wrong.
The biggest difference between a local function call and a network call is that a function executes within the same process and memory space. Once your application communicates with another service, it depends on an entirely separate machine connected over a network.
That network introduces uncertainty.
Packets can be delayed. Connections can time out. Routers can fail. Services can become temporarily unavailable. Even if the destination successfully processes your request, the response might never make it back to your application.
Another thing that surprised me was that a healthy service can still behave like a failing one when it's under heavy load.
Imagine an e-commerce application during a flash sale. Thousands of customers place orders simultaneously. The order service is healthy, but it's suddenly handling far more requests than usual.
Requests begin waiting in queues. Database connections become saturated. Response times increase. Eventually, some requests exceed the client's timeout threshold.
High Traffic
│
▼
Order Service
┌───────────┴───────────┐
▼ ▼
Processing Queue Active Requests
│
▼
Increased Latency
│
▼
Client TimeoutFrom the client's perspective, this looks exactly like a network failure. The request timed out. But the service was simply overwhelmed, not down.
This is another reason distributed systems are difficult. A timeout doesn't tell you whether the service is unavailable, overloaded, or still processing your request. It only tells you that your application stopped waiting for a response.
Now consider what happens when your application calls a payment service and the request times out after five seconds.
Request
App ───────────────────► Payment Service
│
│ (no response)
▼
TimeoutAt first glance, a timeout seems straightforward. The request failed.
But in a distributed system, several very different scenarios can lead to the exact same outcome.
Scenario 1: The request never reached the service. A network interruption, DNS issue, or temporary connectivity problem prevented the request from ever arriving. Since the service never received the request, retrying is the correct thing to do.
Scenario 2: The request was processed successfully, but the response never arrived. The payment service received the request, processed it, and generated a response. However, the response was lost somewhere on its way back due to a network issue or because it exceeded your application's timeout threshold. If you retry now, you might unintentionally charge the customer twice.
Scenario 3: The service failed while processing the request. The payment service received the request but crashed while processing it. Did the payment complete before the crash? Was the database updated? Will the service recover and continue processing?
Although the underlying causes are completely different, your application observes exactly the same thing every time:
Request Timed Out
│
┌─────────────────────┬─────────────────────┐
│ │ │
▼ ▼ ▼
Request Never Response Lost Service Failed
Reached Service After Processing During Processing
│ │ │
└─────────────────────┬─────────────────────┘
▼
Application Can't TellNow you're faced with a decision. Should you retry the request?
If the request never reached the service, retrying is the right thing to do. But if the request was already processed successfully and only the response was lost, retrying could perform the same operation twice. If you choose not to retry, you risk losing the request altogether.
This was one of the biggest mindset shifts for me. In a single application, failures are usually deterministic. In distributed systems, you're often forced to make decisions with incomplete information.
A timeout tells you one thing: your application stopped waiting. Whether the operation succeeded, failed, or is still running remains unknown.
2. Partial Failures Are Silent
In a monolithic application, failures are usually obvious. If the application crashes, everything stops.
Distributed systems behave differently.
Let’s assume your application consists of five independent services. One handles orders. Another manages inventory. A third sends notifications. A fourth generates analytics. A fifth processes payments.
Now imagine the notification service goes down.
Everything else continues running. Orders are still created. Payments are still processed. Inventory is still updated. Only notifications stop working.
At first, this sounds like an advantage. And in many ways it is. But it also means different parts of your system now have different views of reality. One service believes an order is complete. Another hasn't processed it yet. A third is waiting for an event that may never arrive.
These are called partial failures, and they're one of the biggest reasons distributed systems are difficult to reason about. Unlike complete failures, they're often silent. The system appears healthy while only a small part of the workflow is broken. Logs show no errors. Health checks pass. Dashboards stay green.
Detecting these failures is half the problem. The other half is building backend systems with resilience patterns that let the system self-heal without manual intervention.
3. Messages Can Be Delivered More Than Once
Duplicate messages happen in distributed systems because most message brokers prefer delivering twice over losing data entirely.
One of the most surprising production issues I encountered was duplicate processing. At first, I assumed that if an external system sent a request, my application would receive it exactly once. It felt like a reasonable assumption. After all, why would the same event be delivered multiple times?
Webhook Sent
│
▼
Server Times Out
│
▼
Sender Retries
│
▼
Same Event Processed Twice
│
▼
Duplicate NotificationIf the sender doesn't receive a successful response within a certain time, it often retries the request. Sometimes the original request actually succeeded, but the acknowledgment never made it back. From the sender's perspective, it looks like a failure, so it sends the event again.
In practice, message brokers like SQS can redeliver a message if a consumer doesn't acknowledge it within the visibility timeout. A Lambda function processing that message might be invoked again within seconds, well before you realize the first invocation already succeeded.
Suddenly, your application receives the same event twice. If processing that event creates an order, charges a customer, or sends a notification, duplicate processing can have real consequences.
Duplicate events are an expected characteristic of distributed systems. Your system needs to handle them correctly every time they occur.
4. Messages Don't Always Arrive in Order
Events in a distributed system can arrive out of order because different network paths, retries, and queue delays each add unpredictable latency.
When we think about events, we naturally expect them to follow a timeline.
Create an order. Update the order. Ship the order. Deliver the order.
Unfortunately, distributed systems don't always respect that sequence.
Different network paths, retries, queue delays, or independent workers can change the order in which events arrive. An "Order Updated" event might reach your system before the corresponding "Order Created" event.
Actual Timeline Received Timeline
10:00 Order Created 10:01 Order Updated
10:01 Order Updated 10:05 Order Created
│ │
▼ ▼
Network Delay Application Confused
When I first encountered this, it felt counterintuitive. How could something that happened later arrive first?
The answer is straightforward: events are ordered by the network, not by our expectations.
That's why many production systems need mechanisms such as versioning, sequence numbers, or event replay to ensure state remains consistent even when events arrive in unexpected order.
5. Time Isn't as Trustworthy as You Think
As developers, we often trust timestamps without giving them much thought. In distributed systems, I learned that time can be surprisingly deceptive.
Different servers have different clocks. Requests experience different network delays. Two events created milliseconds apart can arrive seconds apart. Even if every machine is synchronized using NTP, slight clock differences still exist.
Consider two services that both update the same order record. Service A sets the status to "shipped" at 10:00:00.003 by its clock. Service B sets it to "cancelled" at 10:00:00.001 by its clock. If you use a "latest timestamp wins" strategy, the order ends up as "shipped" even though the cancellation happened after the shipment update in wall-clock time. The clocks just disagreed by a few milliseconds.
This means "the latest timestamp wins" is often an unsafe conflict-resolution strategy. Just because an event arrives later doesn't mean it happened later.
Once I stopped treating time as an absolute source of truth, many production behaviors that once seemed mysterious started making much more sense.
6. State Lives Everywhere
In a monolithic application, there's often a single source of truth: a database that every part of the application uses.
Distributed systems rarely have that luxury.
Different services maintain their own data because they have different responsibilities. An inventory service knows about stock levels. A notification service knows which messages have been sent. An analytics service stores historical events. Each service has only part of the overall picture.
This decentralization lets each service scale and evolve independently, but it introduces a hard problem: keeping data consistent across the system.
There are moments when different services temporarily disagree about reality, and that's perfectly normal. One service may show an order as "paid" while another still shows it as "processing" because the event hasn't propagated yet. This temporary inconsistency, often called eventual consistency, is a deliberate trade-off that distributed systems make in exchange for better availability and fault tolerance.
Understanding and managing this temporary inconsistency is one of the defining characteristics of distributed system design.
7. Failure Is the Default
The biggest shift in my thinking wasn't learning about queues, retries, or event ordering.
It was realizing that failures are expected, not exceptional.
Services restart. Networks become slow. Workers crash. External APIs return errors. Messages remain unprocessed. All of these things happen regularly in production.
I stopped asking, "How do I prevent failures?" and started asking, "How will my system behave when failures inevitably happen?"
That change in mindset influences almost every architectural decision. Dependable distributed systems come from engineers who assume something will go wrong and plan for it from the start.
The Biggest Mindset Shift: Designing for Failure
When I first started building backend systems, success meant writing code that handled every expected scenario correctly.
As I gained more experience working on production systems, I realized that reliability isn't achieved by handling only the happy path. It's achieved by designing for everything that can go wrong.
That shift changed how I approached system design.
Timeouts became the default assumption, not the edge case. Duplicate delivery became something to handle, not something to hope away. Service unavailability became a design input, not a surprise.
Junior Engineer Experienced Engineer
"Will this work?" → "What happens if..."
• the network fails?
• this runs twice?
• this service is down?
• the response never arrives?
• events arrive out of order?Many of the design patterns used in distributed systems exist because failures are expected, not because they're rare. Retries, dead-letter queues, circuit breakers, health checks, idempotency, event replay, and observability tooling are all responses to the same reality: production systems are constantly operating in imperfect conditions.
The goal is to build systems that continue making progress despite uncertainty.
That's what separates software that works in development from software that holds up in production.
Procedure builds backend systems that handle failure by design, not by luck. If your distributed architecture needs production-grade patterns like idempotency, dead-letter queues, or circuit breakers baked in from day one, talk to our backend team.
What Comes Next
The difficulty of distributed systems has little to do with running multiple services or adopting modern architecture. It comes from letting go of assumptions that are usually true when building software on a single machine.
Networks are unreliable. Messages can be duplicated. Events don't always arrive in order. Services fail independently. Different parts of the system can temporarily disagree about the current state of the world.
Once you understand the nature of these challenges, many production incidents stop feeling mysterious and start feeling predictable.
This article covered why these challenges exist. In the next article, we'll explore one of the most misunderstood concepts in distributed systems: Exactly-Once Processing Is a Myth. We'll look at why duplicate events are inevitable, why "exactly-once processing" is rarely achievable in practice, and how production systems use idempotency to guarantee the correct outcome even when the same request is processed multiple times.
Dependable Systems
│
▼
Expect Failure
│
▼
Detect Failure
│
▼
Recover Gracefully
│
▼
Remain AvailableOne of the biggest lessons distributed systems have taught me is this: reliable systems aren't built because nothing fails. They're built because engineers assume something eventually will.
Have questions about distributed systems challenges or want to discuss what you've hit in production? Reach out to Kshitij on LinkedIn.
Frequently Asked Questions
What Are Distributed Systems Challenges?
Distributed systems challenges are the problems that surface when multiple services communicate over a network. The blog above covers seven of them in detail, from unreliable networks (Section 1) and silent partial failures (Section 2) through to the idea that failure is the expected default (Section 7). Each one introduces uncertainty that doesn't exist when code runs on a single machine.
Why Do Timeouts Happen in Distributed Systems?
A timeout occurs when your application stops waiting for a response from another service. The three-scenario breakdown in Section 1 explains why this is hard: the request may never have arrived, may have succeeded with a lost response, or may have crashed the downstream service mid-processing. Your application can't tell the difference. The decision to retry or not has real consequences either way.
Why Do Duplicate Messages Happen in Distributed Systems?
Section 3 walks through the full mechanics. In short, most systems prefer sending a message twice over losing it entirely. When the original acknowledgment is lost, the sender retries, and your application receives the same event again. Handling duplicates through patterns like idempotency is essential for any production system.
How Do You Design Distributed Systems for Failure?
Designing for failure means building with the assumption that services will crash, networks will be slow, and messages will be duplicated or arrive out of order. Engineers use patterns like retries with exponential backoff, idempotency keys, dead-letter queues, circuit breakers, health checks, and observability tooling to detect problems quickly and recover without manual intervention. The Mindset Shift section above covers the thinking behind this approach.

Kshitij Kumar
SDE2
Kshitij Kumar is a Software Engineer specializing in backend systems, APIs, and e-commerce platforms. He works extensively with Node.js, TypeScript, Shopify, and modern cloud-based architectures, building scalable production-ready applications and integrations. With a strong focus on system design, clean code, and AI-assisted development workflows, Kshitij combines solid engineering fundamentals with practical experience delivering reliable software solutions in fast-paced environments.
