Macroservices
A Macroservice, also known as a Domain Monolith, is an architectural pattern that groups services related to a single, cohesive business domain into a single deployable unit. It represents a middle ground between a large, undifferentiated monolith and a granular, highly-distributed microservices architecture.
The goal is to retain the development velocity and clear ownership boundaries of a monolith within a specific domain, while enforcing strict, loosely-coupled API and event-driven communication between different domains.
The Architectural Pendulum: A Recent History
To understand the value of Macroservices, it's useful to understand the path many engineering organizations have taken.
1. The Pain of the Monolith
We initially moved away from monoliths for valid reasons:
- High Cognitive Load: After years of development, the system becomes a "big ball of mud" that is impossible for any single engineer to fully reason about.
- Tight Coupling: Changes in one part of the system have unintended, cascading consequences in another.
- Risky, Slow Deployments: The entire application must be deployed at once, making releases high-stakes events and discouraging frequent updates.
- Technology Lock-in: The entire system is constrained to a single technology stack, making it difficult to adopt new, better tools for specific jobs.
2. The Promise and Peril of Microservices
Microservices promised to solve this. Each service was small, nimble, and easy to understand within its boundary. However, this architectural freedom came at a high operational cost. The complexity didn't disappear; it moved from the code into the network.
This often leads to the Distributed Monolith, an anti-pattern with the downsides of both architectures:
- Communication Overhead: The simple function call is replaced by a matrix of network calls, API contracts, retries, and service discovery (REST, gRPC, Queues).
- Network Unreliability as a Core Problem: Latency and network partitions are now a primary concern of the application logic itself, not just an infrastructure issue.
- Complex Deployment Choreography: Deployments must be carefully sequenced. Services require specific versions of other services and shared libraries to function, creating a brittle dependency graph.
- Data Consistency and Observability Nightmares: Ensuring data consistency across dozens of services requires complex patterns like the Saga pattern. Tracing a single user request across the system becomes a major engineering challenge.
We really worked hard to try to avoid monoliths but we’ve ended up creating a lot of work for ourselves! Not much of it feels productive though. Most of the above act like a drag on what felt productive at the beginning.
The Synthesis: Macroservices as Domain Boundaries
The Macroservice pattern provides a pragmatic solution by aligning the service architecture with the business architecture. It draws heavily on principles from Domain-Driven Design (DDD), especially the concept of a Bounded Context.
A Bounded Context is the logical boundary within which a specific domain model is defined and applicable. Inside the context, terms are unambiguous. (e.g., inside the "Billing" context, a "Customer" has a payment method; inside the "Support" context, a "Customer" has a history of tickets).
A Macroservice is the technical implementation of a single, well-defined Bounded Context.
Guiding Principles
- High Internal Cohesion: Services and logic within the macroservice are highly related and can be tightly coupled. They can share a database, call each other directly, and be deployed as a single unit. This eliminates vast amounts of internal network and contract overhead.
- Low External Coupling: The macroservice exposes a limited, well-defined, and stable API to the outside world. This is its public contract. Communication between macroservices should be asynchronous and event-driven wherever possible.
- Clear Team Ownership: A single team owns the macroservice. They are responsible for its internal architecture, implementation, and its public contract. This aligns with Conway's Law.
How to Define a Macroservice Boundary
Identifying the "seams" for your macroservices is the most critical part of this design.
- Identify Business Capabilities: Look at your organization. What are the core functions it performs?
Billing
,Identity
,Inventory Management
,Search
,Content Ingestion
. These are strong candidates. - Model with Events: Think about the "facts" or events that occur in your business.
UserSignedUp
,PaymentProcessed
,ArticlePublished
. As described in Building Event-Driven Microservices - Adam Bellemare, these events form the basis of a durable, reusable data communication layer. The services that produce or are the primary authority on these events often belong in the same macroservice. - Establish the Public Contract: The primary interface between macroservices should be an Event Stream.
- Service A doesn't call Service B's REST endpoint to get data.
- Instead, Service B subscribes to the
UserSignedUp
event stream produced by Service A and materializes its own view of the data it needs. - This decouples the services in time and availability. Service A doesn't need to know Service B exists, and a failure in Service B will not cascade to Service A.
Example Workflow
- Identity Macroservice: Owns all logic for user creation, authentication, and permissions. It publishes
UserCreated
,UserUpdated
, andUserDeleted
events to a central event bus like Kafka. - Billing Macroservice: Subscribes to the
UserCreated
event stream to create a new billing record for a user. It performs all logic related to subscriptions and payments and publishesInvoicePaid
andSubscriptionCancelled
events. - Support Macroservice: Subscribes to both
UserCreated
andSubscriptionCancelled
events to create and update its own internal model of a customer for the support team.
In this model, the internal complexity of each domain is contained within its respective monolith (the macroservice), while the communication between them is resilient, scalable, and decoupled.
Further Reading & Resources
- Building Event-Driven Microservices - Adam Bellemare by Adam Bellemare provides an exceptional deep-dive into the patterns required for the communication layer between macroservices.
- Domain-Driven Design - Eric Evans is the foundational text for defining bounded contexts.
- Don’t Build a Distributed Monolith