#Beyond Microservices: Why the Industry is Right-Sizing Its Architecture
Three years ago, you couldn’t get a design document approved in a serious engineering organization without drawing at least a dozen boxes connected by gRPC calls and asynchronous message queues. We were all chasing the "Netflix Dream"—the idea that if we just decoupled our domains enough, we would achieve infinite scalability and developer velocity. But for many, the dream turned into a high-latency, high-cost nightmare. We traded the "Big Ball of Mud" for a "Distributed Mess of Spaghetti," where debugging a single user login requires a Ph.D. in distributed tracing and a $50,000-a-month Datadog subscription.
The industry is currently undergoing what I call "The Great Re-evaluation." We aren't abandoning microservices—they remain a critical tool for specific scales—but we are finally admitting that they are not the default. The pendulum is swinging back toward pragmatism, landing on a sweet spot: the Modular Monolith.
I. The Microservices Hangover
We were promised that microservices would allow teams to move independently. While that is true at the scale of 500+ engineers, for a team of 30, it often results in a "Complexity Tax" that bankrupts productivity.
The most common symptom of this hangover is the Distributed Monolith. This occurs when services are technically separate—living in different repositories and running in different containers—but are so tightly coupled that you cannot deploy Service A without also deploying Service B and C. In this scenario, you have all the overhead of a distributed system (network latency, partial failures, serialization costs) with none of the benefits of independent scaling.
The Hidden Costs of Splitting Too Early
When we split a system into microservices, we often underestimate the operational burden:
- Network Latency & Reliability: Every function call that becomes a network call introduces a point of failure. You now need to implement retries, circuit breakers, and timeouts for things that used to be a 2ms stack operation.
- The Observability Tax: In a monolith, a stack trace tells you exactly what happened. In a distributed system, you need OpenTelemetry, spans, and traces just to figure out why a request returned a 500 error.
- Consistency Struggles: Distributed transactions are hard. Most teams end up with "eventual consistency," which is a fancy way of saying "the data will be wrong for a while, and we hope our SAGA pattern works."
- Cognitive Load: A developer used to be able to run the entire stack on their laptop. Now, they need a 64GB RAM MacBook Pro just to spin up the 15 Docker containers required to run a local integration test.
II. The Rise of the "Modular Monolith"
The Modular Monolith is not the "legacy" code of the 2000s. It is a sophisticated architectural pattern where the application is built as a single deployable unit but is strictly partitioned into independent modules with well-defined interfaces.
Think of it as microservices in a single process. You get the logical separation and domain boundaries, but your communication happens via memory-speed function calls rather than HTTP or gRPC.
Enforcing Boundaries in Code
The key to a successful modular monolith is preventing "leaky abstractions." In a poorly designed monolith, any module can reach into any other module's database tables. In a modular monolith, you enforce boundaries at the language level.
Here is an example of how we might structure a Modular Monolith in TypeScript, using internal and external interfaces to prevent coupling.
// modules/orders/internal/order-logic.ts
// This is internal logic, not accessible outside the orders module.
export class OrderProcessor {
calculateTax(amount: number) {
return amount * 0.08;
}
}
// modules/orders/index.ts
// This is the "Public API" for the module.
import { OrderProcessor } from './internal/order-logic';
export interface OrderPublicAPI {
createOrder(userId: string, items: any[]): Promise<string>;
}
export const OrdersModule: OrderPublicAPI = {
async createOrder(userId: string, items: any[]) {
const processor = new OrderProcessor();
// Logic here...
return "order_123";
}
};
By strictly controlling the index.ts (the barrel file), you ensure that other parts of the system—like the Billing or Shipping modules—can only call the createOrder method. They cannot touch the OrderProcessor or the internal database models directly.
Monolith vs. Microservices: The Comparison
| Feature | Modular Monolith | Microservices |
|---|---|---|
| Deployment | Single unit (Atomic) | Independent (Granular) |
| Communication | In-memory (Fast) | Network/RPC (Slow/Unreliable) |
| Data Consistency | ACID Transactions | Eventual Consistency (SAGA) |
| Operational Effort | Low (Single CI/CD pipeline) | High (Orchestration, Service Mesh) |
| Scalability | Vertical + Horizontal (Whole) | Independent Scaling per Service |
III. Pragmatic Right-Sizing: When to Actually Split
The goal isn't to stay in a monolith forever; it’s to stay in a monolith as long as it’s beneficial. The industry is moving toward "Right-Sizing"—the practice of extracting a service only when a specific pain point justifies the complexity.
The "Pain Points" Framework
If you are considering moving a module out into its own microservice, it should meet at least two of the following criteria:
- Independent Scaling Requirements: The
ImageProcessingmodule consumes 90% of the CPU, while theUserAPIconsumes 5%. SplittingImageProcessingallows you to scale it on high-CPU instances without over-provisioning the rest of the app. - Team Autonomy (Conway’s Law): You have two teams in different time zones working on the same codebase, and they are constantly stepping on each other's toes during deployments or merge conflicts.
- Different Tech Stack Needs: Your main app is in Node.js, but your
FraudDetectionmodule needs specialized Python libraries for machine learning. - Security/Compliance: The
PaymentProcessingmodule needs to be PCI-compliant, and keeping it isolated reduces the scope of your audits.
Avoid the "One Table, One Service" Trap
A common mistake in early microservices adoption was mapping services directly to database tables. This is a recipe for disaster. If your UserUpdate service has to call the ProfileService, AuthService, and EmailService just to change a username, your boundaries are wrong. You haven't built services; you've built a distributed class library.
Instead, split by Business Capability. A "Shipping" service should own everything related to shipping, even if that involves three or four different database tables.
IV. Tooling That Eases the Burden
For those who have reached the scale where microservices are a necessity, the good news is that the ecosystem has matured. We are no longer expected to build the "plumbing" ourselves.
Platform Engineering and Backstage
Modern engineering teams are adopting Platform Engineering to reduce the cognitive load. Tools like Backstage (originally by Spotify) provide a "Software Catalog" that makes it easy to discover who owns which service, where the documentation lives, and what the API spec looks like. This solves the "Discovery" problem that plagues large distributed systems.
Dapr (Distributed Application Runtime)
One of the most exciting tools in the "Right-Sizing" toolkit is Dapr. It provides a sidecar that handles the messy parts of distributed systems—state management, pub/sub, and service invocation—abstracting them away from your code.
// Using Dapr to publish an event without worrying about RabbitMQ/Kafka specifics
import { DaprClient } from "@dapr/dapr";
const client = new DaprClient();
async function completeOrder(orderData: any) {
// Instead of managing a Kafka producer, we just hit the Dapr sidecar
await client.pubsub.publish("order-pubsub", "orders", orderData);
console.log("Order published for downstream services.");
}
By using Dapr, you can write code that feels like a monolith but is ready to behave like a microservice. It handles the retries, the mTLS encryption, and the observability spans automatically.
Service Mesh (Istio/Linkerd)
While I often argue that a Service Mesh is "too much" for most teams, it becomes invaluable when you have 50+ services. It handles the traffic management (Canary deployments, blue-green) and security (mTLS) at the infrastructure layer, so your developers don't have to write custom logic for it in every single service.
V. Conclusion: Architecture is a Spectrum
The "Great Re-evaluation" is not a retreat to the past; it is an evolution toward maturity. We are learning that "Microservices" is not a synonym for "Modern." A well-architected Modular Monolith is often more "modern"—in terms of productivity and cost-efficiency—than a fragmented cluster of tiny services.
Architecture is not about choosing the "best" pattern; it is about choosing the right trade-offs for your current scale. If you are starting a new project in 2024, start with a Modular Monolith. Build clean boundaries. Use internal interfaces. Only when the "network tax" becomes cheaper than the "coordination tax" should you reach for the kubectl command.
Key Takeaways for Your Next Architectural Review:
- Design for Extraction, Not for Distribution: Build your modules as if they could be services, but keep them in one process until they need to be.
- Audit Your "Distributed Monoliths": If you have services that are always deployed together, consider merging them back into a single "Macroservice" to reduce overhead.
- Focus on the Developer Experience (DevEx): If it takes a new hire three days to set up their local environment because of service dependencies, your architecture is failing.
- Infrastructure is not Architecture: Just because you can use Kubernetes doesn't mean your application should be split into 50 pieces. Use platform engineering tools to simplify, not complicate.
- Data Sovereignty First: Ensure each module or service truly owns its data. If you are doing cross-service joins in your application code, your boundaries are misplaced.