Microservices Foundations
1. Monolith vs. Microservices: When to Split, When Not To
A Monolith puts all business logic, data access, and UI (often) into a single deployable unit. Microservices break this up into small, autonomous services that communicate over a network, each owning its own lifecycle.
When to Split (Move to Microservices)
-
Scale Mismatch: Different parts of your app have drastically different resource needs. (e.g., Your image processing logic needs heavy GPU/CPU, but your user profile logic needs almost nothing).
-
Team Velocity Bottlenecks: Multiple independent teams are tripping over each other, fighting for deployment slots, and suffering from massive merge conflicts.
-
Fault Isolation: If a non-critical feature (like a recommendation widget) crashes, you cannot afford to have it take down your entire checkout system.
When NOT to Split (Stick with a Monolith)
-
Early-Stage Startups (No PMF): When you are still figuring out what your product actually does, your domain boundaries will change rapidly. Shifting boundaries in a monolith means refactoring code; shifting them in microservices means rewriting APIs and migrating data networks.
-
Low Domain Complexity: If your app is primarily a CRUD system with low traffic, microservices add unnecessary operational tax (CI/CD pipelines, service meshes, monitoring).
-
Small Teams: If you only have 3–5 engineers, managing 15 microservices means you are spending more time on infrastructure than delivering features.
2. Service Boundaries: Domain-Driven Design (DDD) & Bounded Contexts
The biggest mistake teams make is splitting services by technical layers (e.g., UI service, Logic service, Database service) or by random nouns. Instead, use Domain-Driven Design (DDD).
Bounded Contexts
A Bounded Context is a conceptual boundary where a domain model perfectly applies. Inside the boundary, all terms have a specific, unambiguous meaning.
The “Product” Example:
In the Inventory Context, a “Product” means dimensions, weight, and warehouse shelf location.
In the Marketing Context, a “Product” means a description, promotional images, and a discount code.
In the Billing Context, a “Product” means a price point, tax code, and SKU.
Instead of creating one massive, bloated Product object used by the whole company, DDD suggests creating three separate services (Inventory, Marketing, Billing), each owning its own specific context and definition of a “Product.”
3. Database Per Service vs. Shared Database Anti-Pattern
If your microservices are pointing to the exact same central database, you do not have microservices. You have a distributed monolith.
The Shared Database Anti-Pattern
When services share a database, they become tightly coupled at the data layer.
-
Schema changes break everything: If Team A changes a column name in a shared table, Team B’s service instantly crashes.
-
Bypassing APIs: Services will be tempted to read directly from another service’s tables instead of using their public API, destroying encapsulation.
Database Per Service
Every microservice must completely own its own data store. No other service is allowed to read or write to that database directly; they must go through the service’s public API.
-
Polyglot Persistence: Because they are isolated, your Recommendation Service can use a Graph database (Neo4j), your Catalog Service can use a document store (MongoDB), and your Financial Service can use a relational DB (PostgreSQL).
4. Synchronous vs. Asynchronous Communication
How your services talk to each other dictates how resilient your system will be.
Synchronous (REST / gRPC)
The caller sends a request and blocks, waiting for an immediate response from the receiver.
| Metric | REST (HTTP/JSON) | gRPC (HTTP/2 / Protobuf) |
| Data Format | Human-readable Text (JSON) | Binary (Protocol Buffers) |
| Performance | Slower (payload overhead) | Blazing fast (compact binary, multiplexing) |
| Best Used For | External APIs, frontend-to-backend | Internal service-to-service communication |
-
The Catch: Synchronous communication creates runtime coupling. If Service A calls Service B, which calls Service C, and Service C is down, the entire request chain fails.
Asynchronous (Event-Driven)
The caller emits an event (e.g., “OrderPlaced”) to a message broker (like Apache Kafka or RabbitMQ) and immediately moves on. Other services listen for that event and react whenever they are ready.
-
Pros: Extreme decoupling. If the Billing Service goes down, the Order Service can still accept orders. When Billing comes back online, it processes the backlog of events from the broker.
-
Cons: Eventual Consistency. You have to accept that data across your system won’t be updated instantly everywhere at the exact same millisecond. It also introduces architectural complexity like handling duplicate messages (idempotency).