Software That Models Reality
Reality doesn't wait for you to query it. It just happens. Our most resilient, scalable, and understandable software will be that which accepts this truth and learns to listen.
All worthwhile software exists for one reason: to model and accelerate processes that already exist in the real world.
We don't build inventory systems because we're fascinated by databases; we build them because physical products move, run out, and need to be replenished. We don't create payment systems for the love of transactions; we create them because in the real world, people constantly exchange value for goods and services.
A banking system models how money really works. An e-commerce models how we shop. A CRM models how relationships are built.
The rule is simple: the more faithfully your software reflects the reality it tries to model, the easier it is to understand, maintain, and evolve. Not because it's magically simpler, but because it aligns with patterns that already exist in our minds and in the world.
And here lies the fundamental problem: we often build our systems in a way that directly contradicts how reality actually works.
How the Real World Really Works
The real world doesn't move forward by constantly asking questions. Although we sometimes poll the state of things—is the water boiling yet? has the delivery arrived?—the critical, transformative actions are almost always events. Things just happen, and when they happen, they trigger other things.
Imagine you run a pizzeria. In reality:
A customer walks in and orders → an
OrderReceived
is created.The oven reaches temperature → an
OvenReady
event is emitted.The mozzarella runs out → a
LowInventory
alert is triggered.The supplier arrives with ingredients → the inventory is updated, generating a
StockReplenished
.
Each event occurs exactly when it should. There's no employee obsessively checking every 30 seconds if "anything has changed" when likely nothing has happened.
Reality is inherently reactive and event-driven. It's a natural flow of cause and effect.
How We Build Our Software
Now, imagine that pizzeria run like many of our modern systems:
The result: an employee who must call multiple departments for every single order, waiting on each call, creating a fragile and slow chain of dependencies. If a single department doesn't answer, the entire process grinds to a halt.
This is what we do in software. We build systems that constantly ask other systems "what is your current state?", creating complex and brittle chains of synchronous calls.
The Bias Toward the Unnatural
Why do we fall into this trap?
1. The Tyranny of Request-Response Thinking: HTTP and REST are fantastic tools for their original purpose: transferring the state of resources in a decoupled and cacheable way. The problem isn't REST. The problem is our tendency to use the request-response hammer for every problem, even those that are inherently asynchronous and conversational, like business processes.
2. The Illusion of Microservice Control: The idea of "one service for each thing" is logical until a real business process requires ten of those "independent" services to coordinate. When that coordination is done via synchronous calls, we haven't created microservices; we've created a distributed monolith: just as coupled as a traditional monolith, but with network latency added at every step.
3. The Fear of Visible Complexity: Event-driven systems expose complexity. Causality is explicit. In chained REST calls, complexity is hidden in pyramids of await
and nested if/else
statements. But hidden complexity doesn't disappear; it becomes technical debt with compound interest.
Software That Reflects Reality
Let's go back to the pizzeria, modeled as the world actually works:
The difference? Each component reacts to facts. It is autonomous and decoupled. It doesn't ask, it listens. The system responds to reality instead of interrogating it.
But Reality Also Has a Price
This approach is not a silver bullet. Modeling reality means accepting its inherent messiness. Ignoring these challenges is a recipe for disaster.
1. Eventual Consistency: This is the elephant in the room. When a user pays, their order might not appear as "paid" instantaneously. The state "converges" across the system as events are processed. This requires designing user interfaces that handle this reality (e.g., "Processing your order...") to avoid confusing the user.
2. The Hell of Distributed Debugging: In a synchronous system, an error produces a stack trace. In an asynchronous one, an error produces a forensic investigation. Reconstructing why an order ended up in an anomalous state requires more than console.log
. It demands iron-clad discipline in traceability (with correlationId
and causationId
in every event) and specialized tools (like OpenTelemetry) to follow a flow across multiple services and message queues.
3. The Complexity of Sagas: For long-running business processes (booking a flight, hotel, and car), the Saga pattern is used. If one step fails, the previous ones must be compensated (canceled). Writing robust compensation logic is extremely difficult. What if the payment compensation fails? Without careful design, you can leave the system in an inconsistent state and lose money.
The Moment of Clarity: Your Logs Are Wasted Events
Here is the most powerful realization: your logs are already the history of your business.
Every time you write logger.info("User 123 completed order 456");
, you are recording a domain event. But you are wasting it as dead text in a file that no one will read.
What if, instead, you modeled it as a first-class citizen of your system?
Suddenly, that "log" can:
Update dashboards in real-time.
Trigger the confirmation email.
Feed the analytics system.
Invalidate relevant caches.
All automatically, because you are modeling what actually happened.
Beyond Autopilot: A Call to the Craftsman
At this point, the discussion is no longer about REST vs. message brokers. It's about something deeper: the difference between simply writing code and practicing software engineering as a craft.
It's easy to keep building as we've always been taught: copy the Controller-Service-Repository
pattern, chain some calls, and deliver the feature. It works. No one will get fired for it. But this "autopilot" mode prevents us from growing and building truly robust and elegant systems.
True engineering begins when we ask ourselves a fundamental question before writing a single line of code:
"What is the nature of the problem I am solving?"
Is it a simple state query? Or is it a business process that evolves over time? Is it an atomic action or a conversation between different parts of my system?
Asking these questions is the first step to turning off autopilot. It forces you to think about the domain first and the technology second. It pushes you to explore patterns beyond what you were taught in a bootcamp or found in a 5-minute tutorial. It invites you to see your work not as an assembler of components, but as an architect modeling a complex system.
The path to becoming an exceptional developer isn't about knowing the latest trendy framework. It's about developing the judgment to choose the right tool and the right pattern for the right problem. It's about the relentless curiosity to understand the why behind things, not just the how.
Reality doesn't wait for you to query it. It just happens. Our most resilient, scalable, and understandable software will be that which accepts this truth and learns to listen.