Theoretical Foundations
Welcome to the curriculum workspace. Here you will find long-form technical guidelines outlining core architectural blueprints and implementation mechanics.
Module 8: Monoliths to Domain-Driven Microservices
PREREQUISITE STATEMENT: Read this module after completing Module 7 (CAP & PACELC). Once you understand the physical and consistency limits of distributed networks, you must learn how to design software boundaries that align with these limits, preventing tight integration that turns a distributed system into a fragile "distributed monolith."
1. Monoliths, Modular Monoliths, and Microservices
One of the most critical decisions a systems architect makes is selecting the deployment and execution boundaries of the application codebase. A misaligned boundary architecture leads to slow release velocity, organizational friction, and fragile system execution.
[ Monolith ] [ Modular Monolith ] [ Microservices ]
+----------------------+ +----------------------+ +---------+ +---------+
| App Engine Process | | App Engine Process | | Service | | Service |
| (Shared Database) | | (Schema Isolation) | | A DB | | B DB |
+----------------------+ +----------------------+ +---------+ +---------+
A. The Monolith
A monolithic architecture packages all business capabilities, routing logic, database access modules, and background tasks into a single executable binary or runtime process:
- Pros: Easy to deploy (one artifact), low communication latency (in-memory method calls instead of network hops), simplified debugging, and straightforward transactional guarantees (single-database ACID).
- Cons: Single point of failure (a memory leak in one module crashes the entire process), long build and deployment times, scaling bottlenecks (you must scale the whole process even if only one module is under load), and high organizational coupling (hundreds of engineers committing to a single repository, leading to merge conflicts and release blockages).
B. The Modular Monolith
A modular monolith maintains a single deployable process, but enforces strict, code-level logical separation between modules using language-level access modifiers (e.g., package-private visibility in Java, internal modifiers in C#, or decoupled modules in Go):
- Mechanics: Modules can only communicate with each other through defined public interfaces. Directly referencing another module's internal classes or querying another module's database tables is strictly prohibited. Database schemas are logically separated (e.g. by schema namespace prefixes), and cross-module transactions are avoided.
- Pros: Combines the simple deployment and low latency of a monolith with the organizational clean boundaries and clean interfaces of microservices.
- Cons: Requires constant developer discipline and architectural linting (e.g. using ArchUnit) to prevent boundary leakages. If modularity fails, the codebase decays back into a "big ball of mud."
C. Microservices
Microservices partition business capabilities into physically separate, independently deployable processes that communicate over the network (e.g., via HTTP/REST, gRPC, or messaging queues):
- The Golden Rule: Database-per-Service. A microservice must own its data store. No external service is allowed to read or write directly to its database. All data access must go through the service's public API.
- Pros: Independent deployment cycles (deploy a fix to Service A without touching Service B), technology stack flexibility (Service A can use Go/Postgres while Service B uses Node.js/DynamoDB), and independent scaling.
- Cons: High operational complexity (monitoring, tracing, service mesh routing), latency amplification (network hops replace in-memory calls), distributed data consistency issues (Saga patterns replace local database transactions), and testing friction.
The "Distributed Monolith" Anti-Pattern
A distributed monolith occurs when a team splits a monolithic application into separate network services but retains the tight dependencies of the monolith:
- Symptom A: Services share a single centralized database, reading and writing to the same tables directly. A change to the database schema in one service breaks others.
- Symptom B: Services communicate via synchronous RPC chains (Service A calls B, which calls C, which calls D). If Service D experiences a latency spike, the entire chain stalls, propagating the failure up the stack.
- Result: You inherit all the operational complexity and network latency of microservices, with none of the independent deployment or scaling benefits of decoupled architectures.
2. Strategic Domain-Driven Design (DDD)
To prevent the distributed monolith anti-pattern, you must decompose your system using Domain-Driven Design (DDD). DDD provides the strategic tools to map application boundaries to actual business capabilities rather than technical convenience.
[ Strategic Domain-Driven Design ]
|
+---------------------------------+---------------------------------+
| | |
[ Core Domain ] [ Supporting Domain ] [ Generic Domain ]
- Direct differentiator - Necessary but standard - Standard industry problem
- Allocate top engineers - Custom built - Outsource or SaaS
- Example: Pricing Engine - Example: Inventory Catalog - Example: Identity/Auth
A. Ubiquitous Language
A core pillar of DDD is establishing a Ubiquitous Language — a shared, unambiguous terminology co-created and agreed upon by software developers, product managers, and domain experts.
- This language must be used consistently in conversations, product requirements, database designs, and source code variable names.
- Anti-pattern: Developers using technical jargon (e.g.,
UserRow,PaymentTransactionPayload) while business stakeholders use domain words (e.g.,Customer,Settlement). The translation layer between business and code inevitably introduces bugs.
B. Subdomains
A subdomain is a partition of the business's overall domain. DDD classifies subdomains into three tiers:
- Core Domain: The primary competitive advantage of the organization. This is what the company does better than anyone else, and it must be custom-developed internally. (e.g., Netflix's recommendation algorithm; Stripe's fraud-detection engine).
- Supporting Domain: Operations that are necessary for the business to function but do not act as a market differentiator. These require custom builds but receive less engineering priority. (e.g., an inventory replenishment system for an e-commerce platform).
- Generic Domain: Standard software needs that are identical across industries. These should be solved using off-the-shelf software or SaaS integrations. (e.g., identity access management, billing, email notifications).
C. Bounded Contexts
A Bounded Context is the boundary within which a specific domain model applies and the Ubiquitous Language is valid. Inside a bounded context, terms have a single, precise meaning. In a large enterprise, a single word can mean completely different things to different departments:
[ The Multi-Meaning Domain Object "Product" ]
|
+----------------------+----------------------+
| |
[ Catalog Context ] [ Shipping Context ]
- Price - Weight
- Marketing Description - Dimensions
- Images - Box Type
By defining separate Bounded Contexts (CatalogContext and ShippingContext), we allow each team to build an optimized domain model for their specific tasks, avoiding a single, bloated "Enterprise Product Class" containing 200 unrelated properties.
3. Context Mapping & Relationship Patterns
Because Bounded Contexts must exchange data, you must explicitly define their interactions using Context Mapping. The relationship patterns below establish both code integration structures and team coordination models:
graph LR
subgraph Upstream
OHS[Open Host Service]
end
subgraph Downstream
ACL[Anti-Corruption Layer] --> Client[Domain Services]
end
OHS -->|Published Language / API| ACL
- Shared Kernel: Two bounded contexts share a subset of the domain model and database tables. This represents high coupling and requires both teams to synchronize their build and release schedules. (Use with extreme caution).
- Customer-Supplier (Upstream/Downstream): The downstream context (Customer) relies on the upstream context (Supplier). The supplier must deliver updates that the customer needs. The supplier team is responsible for coordinating changes to protect downstream clients.
- Conformist: The downstream context conforms completely to the upstream context's domain model, adopting its schemas and language directly to avoid translation overhead.
- Anti-Corruption Layer (ACL): A translation layer that sits between the downstream domain and external services. It translates incoming foreign models into the downstream's clean local domain models. The ACL protects the downstream domain from legacy schemas and changing external APIs.
- Open Host Service (OHS) / Published Language (PL): The upstream service provides a stable, documented public API (OHS) using a standard, serialization-agnostic data format (PL, such as JSON or Protobuf) so multiple downstream clients can integrate with it without custom logic.
4. API Communication Protocols: Synchronous Integration
When microservices integrate synchronously, they rely on one of two primary network protocols at the application layer:
A. REST (Representational State Transfer)
- Mechanics: Typically uses HTTP/1.1 or HTTP/2, with JSON payloads. Resources are mapped to URI paths, and actions are determined by standard HTTP verbs (
GET,POST,PUT,DELETE). - Pros: Simple, human-readable, stateless, and globally supported by web browsers and external client developers. Easy to test using tools like Postman or curl.
- Cons: Verbose text serialization (JSON overhead), lack of native schema enforcement (schemas must be documented via OpenAPI/Swagger), and higher latency due to HTTP/1.1 connection blocking.
B. gRPC (Remote Procedure Call)
- Mechanics: Uses HTTP/2 as its transport protocol and Protocol Buffers (Protobuf) as its interface definition language and binary serialization format.
Here is an example of a Protobuf service definition (order_service.proto):
syntax = "proto3";
package orders;
option go_package = "github.com/mpc/orders/v1;ordersv1";
service OrderService {
rpc GetOrderDetails (GetOrderRequest) returns (GetOrderResponse);
}
message GetOrderRequest {
string order_id = 1;
}
message GetOrderResponse {
string order_id = 1;
string customer_id = 2;
double total_amount = 3;
string status = 4;
}
- Pros:
- Extreme Efficiency: Protobuf encodes data into compact binary payloads, using a fraction of the bandwidth of JSON.
- Multiplexing: HTTP/2 allows sending multiple concurrent requests and responses over a single TCP connection, reducing handshake latency.
- Strong Typing: Code stubs and clients are automatically compiled from the
.protoschemas across multiple languages (Go, Java, Rust, Node.js).
- Cons: Not directly readable by humans (requires custom tools to decode binary payloads), and browser support requires proxy layers (gRPC-Web).
5. Domain Discovery: Event Storming
Before writing APIs or drawing database diagrams, architects and product teams must conduct domain discovery. The industry-standard workshop format for this is Event Storming, created by Alberto Brandolini.
Event Storming is a rapid, collaborative modeling process that uses color-coded sticky notes to map out the chronological flow of a business domain:
[ orange: Domain Event ] <-- triggered by -- [ blue: Command ] <-- executed on -- [ yellow: Aggregate ]
|
produces view
|
v
[ green: Read Model ]
Event Storming Sticky Note Legend
| Color | Element Type | Description | Example |
|---|---|---|---|
| Orange | Domain Event | Something that has happened in the business domain. Written in the past tense. | OrderSubmitted, PaymentProcessed |
| Blue | Command | A decision or action triggered by a user, system, or scheduling timer. | SubmitOrder, ProcessPayment |
| Yellow | Aggregate | The business entity or boundary that accepts commands and emits events. | Order, InventoryItem |
| Green | Read Model | A view or UI screen that provides information to users to help them execute commands. | ProductCatalog, InvoiceView |
| Lilac | Policy / Rule | An automated rule or reaction that triggers a command when a domain event occurs. | "When InventoryLow, trigger CreateReorder" |
6. Documenting Boundaries: The Context Map Specification
An enterprise architecture must maintain a clear context map. Below is an example of a Domain Boundary & Context Map Specification mapping the interactions between three core domains:
graph TD
subgraph Enterprise Context Map
OrderCtx[Order Context] -->|gRPC Call| CustomerCtx[Customer Context]
BillingCtx[Billing Context] -->|ACL Translation| OrderCtx
end
1. Bounded Context Interface Matrix
| Downstream Bounded Context | Upstream Bounded Context | Relationship | Communication Protocol | Interface Contract (Data Schema) |
|---|---|---|---|---|
| Order Context | Customer Context | Customer-Supplier | gRPC (HTTP/2) | GetCustomerDetails(CustomerId) -> CustomerDetailsResponse (Protobuf v1.4) |
| Billing Context | Order Context | Conformist | REST (HTTP/JSON) | POST /orders/checkout (JSON Schema v2.1) |
| Shipping Context | Order Context | Anti-Corruption Layer | Asynchronous MQ | OrderPlacedEvent mapped to local DeliveryRequest domain model. |
2. Integration Boundary Rules
- Direct database sharing between the
Orderschema andBillingschema is strictly prohibited. All queries must resolve via APIs. - The
Billing Contextmust isolate its internal logic from theOrder ContextAPI using an Anti-Corruption Layer to prevent pricing recalculation changes from altering the core ledger.
7. Hands-on Architecture Challenge
Scenario Description
A legacy monolithic application (MonolithApplication) has a shared PostgreSQL database. Both the UserService and the OrderService run in the same memory process and perform direct SQL queries and table joins across both the Users and Orders tables. This shared database schema makes deployments slow and risky.
Your Goal:
- Deconstruct the monolith into two distinct Bounded Contexts:
UserContextandOrderContextusing appropriate boundary boxes. - Inside the
UserContext, place aUserServicecommunicating with a dedicatedUsersDatabase. - Inside the
OrderContext, place anOrderServicecommunicating with a dedicatedOrdersDatabase. - Draw a dependency arrow showing the
OrderContextcalling theUserContextvia an API request (label itgRPC (Customer-Supplier)). - Modify the architecture diagram diagram to reflect this clean DDD Context Map boundary.
8. Practice Challenge Template
Use this template in your sandbox to visualize the migration:
graph TD
subgraph Legacy_Monolith [Legacy Monolith - Shared DB]
MonolithApp[Monolith Process] -->|Direct SQL / Joins| SharedDB[(Shared Database - Users & Orders)]
style MonolithApp fill:#faa,stroke:#333,stroke-width:2px
style SharedDB fill:#faa,stroke:#333,stroke-width:2px
end
subgraph Target_Microservices [Target Microservices - DDD Boundaries]
subgraph User_Context [User Context]
UserService[User Service] -->|SQL| UsersDB[(Users DB)]
end
subgraph Order_Context [Order Context]
OrderService[Order Service] -->|SQL| OrdersDB[(Orders DB)]
end
OrderService -->|gRPC Call Customer-Supplier| UserService
style UserService fill:#9f9,stroke:#333,stroke-width:2px
style OrderService fill:#9f9,stroke:#333,stroke-width:2px
style UsersDB fill:#9f9,stroke:#333,stroke-width:2px
style OrdersDB fill:#9f9,stroke:#333,stroke-width:2px
end
NEXT MODULE BRIDGE: Once your business domains are separated into Bounded Contexts, synchronous RPC requests can introduce latency bottlenecks and cascading errors. Proceed to Module 9: Event-Driven Architectures (EDA) to discover how to achieve complete boundary decoupling using asynchronous message logs and event streams.