The Future of Microservices in a Post-Monolith World
Two years ago, I led the effort to break our company's aging monolith into microservices. It was the standard story: the monolith had become unwieldy, deployments were risky all-day affairs, and different teams kept stepping on each other's toes. The microservices transformation was painful but ultimately successful—teams could deploy independently, scaling became more granular, and we finally escaped dependency hell.
But something interesting happened along the way. As the industry accumulated more experience with microservices, the conversation began to shift. The pendulum that had swung hard toward extreme service decomposition started to swing back. Teams realized that the complexity they had pushed out of their codebases had reappeared in their infrastructure. The "microservices tax" was real, and sometimes steep.
Today, I want to share where I see microservices architecture heading, based on both my team's journey and broader industry trends. We're entering what I call the "post-monolith world"—where we've collectively learned enough hard lessons to move beyond the false monolith/microservices dichotomy.
The Evolution of Service Architecture
To understand where we're going, let's quickly recap how we got here:
Phase 1: The Monolithic Era (1990s-2010s)
The traditional monolith was simple conceptually—one codebase, one database, one deployment unit. It offered:
- Simple local development and debugging
- Transactional consistency by default
- Straightforward deployment (in theory)
- No network latency between components
But as applications grew, the downsides became crippling:
- Long build times
- Risky deployments
- Technology lock-in
- Scaling bottlenecks
- Organizational friction
Phase 2: The Microservices Revolution (2010s-present)
Pioneered by companies like Netflix, Amazon, and Uber, microservices offered a compelling alternative:
- Independent deployability
- Technology diversity
- Team autonomy
- Fine-grained scalability
- Failure isolation
But as many discovered, microservices introduced significant complexity:
- Distributed transactions
- Network unreliability
- Data consistency challenges
- Monitoring and debugging complexity
- Operational overhead
Phase 3: The Post-Monolith World (Emerging)
Now we're entering a more nuanced phase, where teams are adopting hybrid approaches and making more deliberate architectural choices. The key insight is that service boundaries should reflect business needs, team structures, and specific technical requirements—not dogmatic rules about service size.
Key Trends Shaping the Future
Several trends are influencing how we'll build distributed systems in the coming years:
1. The Rise of the "Right-Sized Service"
We're moving away from the "micro" in microservices. After years of splitting services only to later recombine them, teams are finding that optimal service boundaries aren't as fine-grained as once thought.
A right-sized service:
- Maps to a bounded context within the business domain
- Can be maintained by a single team
- Has minimal runtime dependencies on other services
- Offers reasonable deployment times and developer ergonomics
Here's a schema I've found helpful for determining service boundaries:
1. Map your business domains and subdomains:
- Core domains: Key business differentiators
- Supporting domains: Important business functions
- Generic domains: Necessary but commoditized functionality
2. Identify team boundaries and cognitive load:
- Team size and expertise
- Communication overhead between teams
- Cognitive load of different service scopes
3. Analyze runtime dependencies:
- Synchronous call patterns
- Data consistency requirements
- Performance critical paths
4. Prioritize service autonomy over service count
For example, in our e-commerce platform, we initially separated product catalog and inventory into different microservices because they seemed like distinct concerns. However, the constant synchronization overhead and transactional requirements made this separation more trouble than it was worth. We eventually recombined them into a "Product Domain Service" that better reflected the business reality.
2. From Synchronous to Asynchronous Communication
Early microservices architectures often relied heavily on synchronous HTTP calls, leading to fragile call chains and latency issues. The future belongs to more asynchronous patterns:
- Event-driven architectures
- Message queues and brokers
- Eventual consistency where appropriate
- CQRS (Command Query Responsibility Segregation)
Here's a before/after example of refactoring from synchronous to asynchronous communication:
Before (Synchronous):
// Order Service
async function createOrder(orderData: OrderData): Promise<Order> {
// Create the order
const order = await orderRepository.save(orderData);
// Synchronously call inventory service
const inventoryUpdated = await inventoryClient.reserveItems(order.items);
if (!inventoryUpdated) {
throw new Error('Failed to reserve inventory');
}
// Synchronously call payment service
const paymentProcessed = await paymentClient.processPayment(order.id, order.amount);
if (!paymentProcessed) {
// Rollback inventory
await inventoryClient.releaseItems(order.items);
throw new Error('Payment failed');
}
return order;
}
After (Event-Driven):
// Order Service
async function createOrder(orderData: OrderData): Promise<Order> {
// Create order in PENDING state
const order = await orderRepository.save({
...orderData,
status: 'PENDING'
});
// Publish event for other services to react to
await eventBus.publish('OrderCreated', {
orderId: order.id,
items: order.items,
amount: order.amount,
timestamp: new Date()
});
return order;
}
// Inventory Service (subscribes to OrderCreated)
async function handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
try {
await inventoryRepository.reserveItems(event.items);
// Publish success event
await eventBus.publish('InventoryReserved', {
orderId: event.orderId,
timestamp: new Date()
});
} catch (error) {
// Publish failure event
await eventBus.publish('InventoryReservationFailed', {
orderId: event.orderId,
reason: error.message,
timestamp: new Date()
});
}
}
// Order Orchestrator (subscribes to various events to track order state)
async function updateOrderStatus(event: Event): Promise<void> {
// Track order state based on received events
// Update order status when all required steps are completed
}
This event-driven approach decouples services temporally, improving resilience and scalability at the cost of eventual consistency. The key is recognizing which parts of your system require strong consistency and which can tolerate eventual consistency.
3. The "Service Mesh" Layer
As service architectures mature, there's a growing recognition that much of the "microservices tax" can be externalized to a dedicated infrastructure layer—the service mesh.
Modern service meshes provide:
- Transparent service discovery
- Automatic TLS between services
- Load balancing and circuit breaking
- Observability and traffic control
- Retry and timeout policies
A common implementation with Istio looks like this:
# Service mesh configuration using Istio
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: orders-service
spec:
hosts:
- orders-service
http:
- route:
- destination:
host: orders-service
subset: v1
weight: 90
- destination:
host: orders-service
subset: v2
weight: 10
retries:
attempts: 3
perTryTimeout: 0.5s
timeout: 1s
fault:
delay:
percentage:
value: 0.1
fixedDelay: 0.5s
By pushing these concerns down to the infrastructure layer, your application code becomes cleaner, focusing only on business logic while networking concerns are handled consistently across services.
4. The "Modular Monolith" Renaissance
Perhaps the most interesting trend is the rehabilitation of the monolith—not the tangled legacy systems of the past, but intentionally designed "modular monoliths."
A modular monolith:
- Has clear internal boundaries between domains
- Enforces module encapsulation at the code level
- Provides the deployment simplicity of monoliths
- Avoids distributed systems complexity for smaller teams
Here's how a modular monolith might be structured using TypeScript and NestJS:
// src/modules/orders/orders.module.ts
@Module({
imports: [
// Only import what this module needs
DatabaseModule,
ConfigModule,
// Explicit dependency on other domain modules
PaymentModule.forFeature(), // Limited API exposed by forFeature()
],
controllers: [OrdersController],
providers: [
OrdersService,
OrdersRepository,
// Internal implementation details
OrderValidator,
OrderMapper,
],
exports: [
// Carefully controlled public API
OrdersService,
],
})
export class OrdersModule {}
The key insight here is that many benefits of microservices can be achieved through good modularity practices without incurring all the distributed systems complexity.
Several of my clients have actually migrated from a tangled web of microservices back to a more maintainable modular monolith, particularly for applications that simply didn't warrant the operational complexity of microservices.
5. Polyglot Persistence Done Right
Early microservices architectures often advocated for per-service databases, leading to complex data synchronization needs. We're now seeing more nuanced approaches to data architecture:
- Shared databases for closely related services
- Data services that abstract storage concerns
- Purpose-built storage for specific data patterns
- CDC (Change Data Capture) for data synchronization
For example, rather than giving every service its own database, consider this more pragmatic approach:
1. Group services by data affinity and consistency needs
2. Create "data domains" with appropriate persistence
3. Use CDC tools like Debezium to capture changes for cross-domain needs
4. Expose focused data APIs when cross-service direct access is needed
In our case, we replaced 12 separate databases with 4 domain-oriented databases and a unified CDC pipeline that streams changes to services that need cross-domain data. This significantly reduced operational complexity while maintaining appropriate service boundaries.
From Theory to Practice: Real-World Implementation
Let's examine how these principles look in the real world:
Uber's "Domain-Oriented Microservice Architecture" (DOMA)
Uber, one of the early microservices pioneers, encountered scaling issues with their initial approach. Their services became too granular, leading to excessive network calls and operational complexity.
Their response was DOMA, which advocates for:
- Domain-oriented services rather than function-oriented decomposition
- Standardized service templates to enforce best practices
- Shared infrastructure for cross-cutting concerns
- Clear team ownership of domains
This has allowed Uber to retain the benefits of microservices while reducing the "microservices tax."
Shopify's "Modular Monolith with Rails Engines"
Shopify, powering a significant portion of e-commerce worldwide, has deliberately maintained a modular monolith approach. They use Rails Engines to enforce modularity while avoiding the complexity of a distributed system.
This approach has served them well even at extreme scale—proving that microservices aren't the only path to scalability.
Key techniques they use:
- Strict module boundaries enforced through code reviews
- Focused database access through repository patterns
- Clear ownership of modules by teams
- Horizontal scaling of the entire application
Their success demonstrates that a well-designed monolith can scale remarkably well for certain types of applications.
The Decision Framework: Choosing Your Architecture
Based on my experience guiding multiple organizations through these transitions, I've developed a decision framework for choosing between architectural approaches:
Questions to Ask:
-
Team Structure:
- How many engineering teams do you have?
- How do teams communicate and coordinate?
- Are teams aligned with business domains?
-
Scale Requirements:
- Do different components need to scale independently?
- What are your performance SLAs?
- What is your expected growth trajectory?
-
Development Velocity:
- How frequently do you need to deploy?
- How critical is independent deployability?
- What is your tolerance for coordination overhead?
-
Operational Capacity:
- Do you have expertise to operate distributed systems?
- What is your monitoring and observability maturity?
- What is your error budget and reliability requirement?
Architecture Decision Matrix:
Factor | Modular Monolith | Right-Sized Services | Microservices |
---|---|---|---|
Team Size | Small (under 10 engineers) | Medium (10-50 engineers) | Large (50+ engineers) |
Deployment Frequency | Daily or weekly | Multiple times per week | Multiple times per day |
Scale Requirements | Moderate | High for specific components | Extreme |
Domain Complexity | Low to moderate | Moderate to high | High |
Ops Expertise | Beginner to intermediate | Intermediate | Advanced |
A Hybrid Example
Most modern systems are converging on hybrid architectures. Here's an example from a recent e-commerce platform I helped design:
E-commerce Platform Architecture:
1. Core Shopping Experience: Modular Monolith
- Product Catalog Module
- Shopping Cart Module
- User Profiles Module
- Checkout Flow Module
→ Single deployment unit with clear internal boundaries
→ Shared database for transactional consistency
2. Domain-Specific Services:
- Inventory Service
- Pricing Service
- Payment Processing Service
→ Independently deployable
→ Specialized storage per service need
3. Edge Functions for Customization:
- A/B Testing Logic
- Personalization Rules
- Market-specific Logic
→ Deployed to global edge network
→ Extremely fine-grained deployment
This hybrid approach allowed the core business logic to maintain consistency while giving specific domains the independence they needed.
The Path Forward: Pragmatic Service Design
As we move into this post-monolith world, here are my recommendations for teams building distributed systems:
-
Focus on domains, not size
- Organize around business capabilities
- Let team cognitive capacity guide service scope
-
Embrace architectural pluralism
- Use monolithic approaches where appropriate
- Use microservices where beneficial
- Mix architectures within a single system
-
Reduce the "microservices tax"
- Adopt service mesh for cross-cutting concerns
- Standardize observability and resilience patterns
- Create internal developer platforms
-
Design for evolution
- Build services that can be combined or split later
- Focus on clear interfaces between components
- Maintain good modular practices regardless of deployment model
-
Make deliberate data decisions
- Accept that data is hard to distribute
- Group services with strong data affinity
- Use event-driven patterns for cross-domain data needs
Conclusion: Beyond the Pendulum
The microservices journey has been a classic example of the technology pendulum at work—from monoliths to extreme service decomposition and now toward a more balanced middle ground.
The most successful organizations I work with aren't religiously committed to any specific architectural style. Instead, they apply architectural thinking to solve business problems, choosing the right approach for their specific context, team structure, and technical requirements.
For my team, our once-revolutionary microservices transformation now seems almost quaint. We've recombined some services, split others, and generally focused on drawing boundaries that make sense for our business and team structure. The result is a more pragmatic, maintainable system that still delivers on the original promises of our microservices journey.
The future of services isn't micro or monolithic—it's right-sized, domain-oriented, and pragmatically designed for the problems at hand.
What architecture patterns are you seeing success with? Have you found the modularity sweet spot for your organization? I'd love to hear about your experiences in the comments below.