Don't Build a Distributed System Until You're Forced To

Architectural premature optimization kills more projects than slow code ever will. When you start with complex asynchronous event systems, you trade iteration speed for hypothetical performance. The winning strategy is to start with a monolith using simple function calls, move to synchronous HTTP when network boundaries are required, and only adopt asynchronous events when scale-related bottlenecks leave you no other choice.
I have watched teams spend weeks configuring Kafka clusters and defining protobuf schemas before they even have a working login page. They think they are being 'proactive' by building for 'scale,' but they are actually just adding friction to every change they need to make. When you are in the early stages of a project, your biggest enemy is not a 500ms latency spike; it is the inability to change your mind.
Starting with an 'eventy' architecture means every refactor requires updating producers, consumers, and message brokers. It is the most dangerous form of premature optimization because it locks your logic into a distributed cage before you even know if the logic is correct.
Why should I start with a monolith instead of microservices?
You should start with a monolith because internal function calls are the fastest and most flexible way to iterate on a codebase. In a monolithic structure, refactoring a shared module is a simple IDE action, whereas changing a distributed service requires coordinated deployments and complex versioning strategies.
Imagine you have a billing module and a user module. If they are just two folders in the same repo, moving logic between them is trivial. If they are two separate services, you now have to deal with serialization, network failure modes, and the headache of distributed tracing just to see why a user's plan didn't update. You are essentially paying a 'distributed tax' on every line of code you write. For a new project, that tax is often enough to bankrupt your timeline.
When is the right time to introduce synchronous network calls?
Introduce synchronous HTTP calls only when you have a clear requirement to decouple the deployment cycles or the physical hardware of two components. This usually happens when one part of the system has drastically different scaling needs or when a separate team needs to own a specific domain independently.
Moving to HTTP is the middle ground. It introduces the network, which means you have to start thinking about timeouts and retries, but the execution flow remains predictable. You send a request, you wait for the response, and you handle the result. It is significantly more complex than a function call, but it does not yet require the mental gymnastics of eventual consistency that come with full asynchronous systems.
When does an asynchronous event-driven system become necessary?
Asynchronous event systems are necessary only when you hit a performance wall where synchronous execution blocks the user experience or exceeds the resource capacity of your web servers. You reach for tools like SQS, RabbitMQ, or Kafka when you need to offload heavy processing or handle massive spikes in traffic that would otherwise crash your API.
If you have a process that takes thirty seconds to run, you cannot keep an HTTP connection open and expect the user to wait. That is a legitimate reason to push an event to a queue and handle it in the background. But if you are using an event bus just to send a 'welcome email' on a low-traffic app, you are over-engineering.
The Hierarchy of Architectural Escalation
To keep your project moving fast, follow this progression. Do not skip a step unless you have the data to justify the complexity.
| Architecture Stage | Communication Method | Primary Benefit | Complexity Level |
|---|---|---|---|
| Monolith | Local Function Calls | Maximum iteration speed and easy debugging | Low |
| Distributed Sync | Synchronous HTTP/gRPC | Decoupled deployments and team autonomy | Medium |
| Distributed Async | Event Bus / Queues | Massive scale and high fault tolerance | High |
How do I transition between these stages without a total rewrite?
The trick is to write modular code within your monolith from day one. Keep your domains separated and avoid 'spaghetti' dependencies. If your billing logic is isolated, turning a local function call into a network call later is a surgical change rather than a ground-up rewrite.
// 1. Start here: Simple local function
const receipt = await billing.process(order);
// 2. Scale here: Synchronous network call
const response = await axios.post('https://billing-api/process', order);
// 3. Final form: Asynchronous event
await sqs.sendMessage({ queueUrl: 'billing-tasks', messageBody: order });
By following this path, you ensure that you are only adding complexity when your product's success demands it. Performance is a feature, but in the beginning, flexibility is your only lifeline. Stop worrying about 'cool' event-driven systems and start writing functions that work.
FAQ
Does starting with a monolith mean my app won't scale?
No. Many of the largest sites on the internet started as monoliths and scaled significantly before ever splitting into services. You can scale a monolith vertically or by running multiple instances behind a load balancer long before you need a distributed event architecture.
How do I know if I am over-engineering my architecture?
If you spend more time debugging network issues, message serialization, or environment configurations than you do writing business logic, you are over-engineering. If a simple change to a data field requires updating three different repositories, you have optimized for performance too early.
Is it harder to move from a monolith to events later?
It is a challenge, but it is a 'good' challenge to have because it means your app is successful enough to require it. It is much easier to split a well-organized monolith than it is to fix a broken, over-complicated distributed system that was built on top of shifting requirements.




