๐งฉ Microservices Principles
A single big application is easy to start and hard to grow. Microservices flip that trade: harder to start, easier to grow โ if you have the problems that justify the cost. This lesson teaches you what a microservice actually is, why teams adopt them, and the honest reasons you often shouldn't.
Week 14 · Monday: Microservices Architecture · Lecture 1
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Define a microservice as an independently deployable service built around a business capability
- Contrast microservices with a monolith and name the specific trade-offs of each
- Explain the "database per service" rule and why one service must never touch another's tables
- Use bounded contexts from Domain-Driven Design to draw sensible service boundaries
- Decide, for a given project, whether microservices are worth their operational cost
- Recognize the "distributed monolith" anti-pattern and how to avoid it
Estimated Time: 70 minutes
Practice: Decompose an online-learning platform into services and justify each boundary.
In This Lesson
What Is a Microservice?
A microservice is a small, independently deployable service that owns one business capability end-to-end โ its own code, its own data, and its own deployment lifecycle. A microservices architecture is an application built as a collection of such services that cooperate over the network.
Picture a busy restaurant kitchen. Instead of one chef juggling appetizers, mains, and desserts, there are specialized stations: the grill, the salad prep, the pastry counter. Each station does one job well, talks to the others when it needs something ("two steaks ready for table six"), and can be staffed up independently โ three extra salad hands during the lunch rush without touching the grill. Microservices apply that same division of labor to software.
The word "micro" is misleading. The unit of size is not lines of code โ it's responsibility. A microservice is as big as one business capability needs it to be. "Small enough that one team can own it, large enough to be worth deploying on its own" is the useful measure.
๐ The defining property: independent deployability
If you can't ship one service to production without coordinating a release of the others, you don't have microservices โ you have a distributed monolith with all the costs and none of the benefits. Everything else in this lesson exists to protect independent deployability.
Monolith vs. Microservices
In a monolith, all the code โ UI, business logic, data access โ lives in one deployable unit, sharing one process and usually one database. In a microservices system, that same functionality is split across many services, each shipped and scaled on its own.
Neither is "better" in the abstract. They optimize for different things:
| Dimension | Monolith | Microservices |
|---|---|---|
| Getting started | Fast โ one repo, one deploy | Slow โ pipelines, discovery, infra per service |
| Calling other code | In-memory function call (fast, reliable) | Network call (latency, can fail) |
| Scaling | Scale the whole app together | Scale hot services independently |
| Deploying a change | Redeploy everything | Redeploy one service |
| Team autonomy | Teams coordinate in one codebase | Teams own services end-to-end |
| Data consistency | Easy โ one ACID database | Hard โ eventual consistency, sagas |
| Debugging a request | One stack trace | Distributed tracing across services |
โ ๏ธ Don't start with microservices
Martin Fowler's "monolith first" advice still holds: begin with a well-structured monolith, learn where the real boundaries are, and extract services only when a boundary is stable and the pain of not splitting is concrete. Premature microservices lock in guesses about boundaries you don't yet understand โ and getting a boundary wrong is far more expensive across a network than inside one process.
Core Principles
Across every successful microservices system, the same handful of principles show up. They all reinforce one goal: services that can change and ship without waiting on each other.
1. Organize around business capability
A service should map to something the business does โ "manage the product catalog," "process payments" โ not to a technical layer like "the database service." Capability-aligned services change together with the business need they serve, so a feature usually touches one service, not five.
2. Own your data
Each service is the sole owner of its data. Other services ask for it through the API; they never reach into its tables. (Full section below.)
3. Communicate over well-defined APIs
The API is the contract. Version it so you can evolve without breaking callers.
// Product Service โ a stable, versioned REST contract
// GET /api/v1/products List products (supports ?category & ?limit)
// GET /api/v1/products/:id Fetch one product
// POST /api/v1/products Create a product
// PUT /api/v1/products/:id Replace a product
// DELETE /api/v1/products/:id Remove a product
// The /v1/ prefix lets you ship /v2/ later WITHOUT breaking existing clients.
app.get('/api/v1/products/:id', async (req, res) => {
const product = await productRepo.findById(req.params.id);
if (!product) return res.status(404).json({ error: 'Product not found' });
res.json(product);
});
4. Deploy independently
Each service has its own CI/CD pipeline. Updating the login flow ships the User service alone โ the other 40 services keep running untouched.
5. Design for failure
Networks drop packets and services crash. A resilient service degrades gracefully instead of taking the whole system down with it. Timeouts, retries, and circuit breakers are the standard tools โ we cover them in depth in the next lesson.
// A minimal circuit breaker: stop hammering a service that's clearly down.
class CircuitBreaker {
constructor(action, { failureThreshold = 3, cooldownMs = 10000 } = {}) {
this.action = action;
this.failureThreshold = failureThreshold;
this.cooldownMs = cooldownMs;
this.failures = 0;
this.state = 'CLOSED'; // CLOSED = healthy, OPEN = failing fast
this.openedAt = 0;
}
async fire(...args) {
if (this.state === 'OPEN') {
// Has the cooldown elapsed? If so, allow ONE trial request.
if (Date.now() - this.openedAt < this.cooldownMs) {
throw new Error('Circuit open โ failing fast');
}
this.state = 'HALF_OPEN';
}
try {
const result = await this.action(...args);
this.reset(); // success โ the service is healthy again
return result;
} catch (err) {
this.recordFailure();
throw err;
}
}
recordFailure() {
this.failures += 1;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.openedAt = Date.now();
}
}
reset() { this.failures = 0; this.state = 'CLOSED'; }
}
6. Decentralize technology choices
Because services only meet at the API, each team can pick the right stack for its job โ Node.js for an I/O-heavy notification service, Python for an ML-driven recommender โ without forcing that choice on anyone else. Use this freedom deliberately, though: every extra language is another thing to build, monitor, and hire for.
Database per Service
This is the principle beginners break first and regret most. Each service owns its data privately. No other service โ and no shared "reporting" job โ reads or writes those tables directly. Access goes through the owning service's API, always.
Why so strict? Because a shared database quietly recouples everything you worked to decouple:
- Independent deploys die. If two services share a table, a schema change forces a coordinated release โ the exact thing microservices exist to avoid.
- Boundaries blur. Once any service can read any table, business rules leak everywhere and no one owns the data's integrity.
- Freedom of technology returns. The Product service can use MongoDB while Orders uses PostgreSQL, because nothing else depends on either schema.
The same word can even mean different things in different services, and that's fine:
- In the Catalog service, a "Product" is a name, description, images, and category.
- In the Inventory service, a "Product" is a SKU, stock level, and warehouse location.
- In the Order service, a "Product" is the price captured at purchase time and the quantity ordered.
๐ก "But I need data from two services at once"
That's normal. You have three good options: (1) have one service call the other's API and combine the results, (2) let services publish events so each keeps a local read-copy of what it needs, or (3) aggregate at an API gateway. What you must not do is JOIN across service databases. The next two lessons cover options 1 and 3 in detail.
Finding Boundaries with DDD
The hardest question in microservices is not "how do I build a service?" but "where does one service end and the next begin?" Split too finely and you drown in network chatter; split too coarsely and you're back to a monolith. Domain-Driven Design (DDD) gives you a principled way to draw the lines.
The key idea is the bounded context: a boundary within which a particular model and vocabulary are consistent. Inside the "Shipping" context, everyone โ code and people โ means the same thing by "address" and "delivery." Cross into "Billing" and the same words carry different meaning. Each bounded context is a strong candidate for a service.
Two supporting concepts make bounded contexts practical:
- Ubiquitous language โ the shared vocabulary of a context, used identically by domain experts and in the code. When the warehouse team says "pick," the Inventory service has a
pick()operation. - Context map โ a diagram of how contexts relate: who calls whom, who publishes events, where translation between models happens.
โ A rule of thumb for boundaries
If two pieces of functionality almost always change together and constantly need each other's data, they probably belong in the same service. If they change on different schedules for different reasons and rarely need each other, they're candidates to split. Coupling and cohesion, applied at the service level.
When (Not) to Use Microservices
Microservices buy you scalability and team autonomy at the price of real operational complexity. That trade is worth it only sometimes.
Good fit
- Large applications where a monolith has become painful to change and deploy
- Clear, stable domain boundaries you actually understand
- Components with wildly different scaling needs (a search service hammered while billing idles)
- Multiple teams that need to ship on independent schedules
Poor fit
- Small apps where the infrastructure overhead dwarfs the benefit
- Early-stage products still discovering what they even are โ you don't know the boundaries yet
- Teams without solid DevOps, CI/CD, and observability practices in place
- Domains whose components are so interconnected that every request would fan out to a dozen services
โ ๏ธ The distributed monolith โ the worst of both worlds
The classic failure is splitting into services that are still tightly coupled: they share a database, must deploy together, and every user action triggers a chain of synchronous calls. Now you have network latency, partial failures, and hard debugging โ plus the coordination cost of a monolith. If your "microservices" can't deploy independently, you built a distributed monolith. The fix is data ownership and looser, often asynchronous, communication.
The pragmatic path most teams take is the monolith-first, extract-gradually approach:
Shopify and Amazon both grew this way โ starting monolithic and carving out services only where scaling or team boundaries demanded it, keeping plenty in the monolith where it made sense.
Practice & Quiz
๐๏ธ Exercise 1: Decompose a platform
Goal: You're designing an online-learning platform with these features: user registration and login, a course catalog with search, enrollment and progress tracking, video delivery, quizzes, discussion forums, notifications, payments, and analytics. Sketch a set of microservices, name each service's single responsibility, and identify which service owns which data.
๐ก Hint
Look for bounded contexts โ clusters of features that share a vocabulary and change together. "Enrollment" and "progress" belong together; "payments" is clearly its own context. Give each service exactly one reason to change, and remember: each service owns its own database.
โ Solution
One reasonable decomposition, each service owning its own store:
- User Service โ registration, authentication, profiles โ User DB
- Catalog Service โ course details, search โ Course DB
- Enrollment Service โ enrollments, progress tracking โ Enrollment DB
- Content Service โ video streaming and content metadata โ Content DB
- Assessment Service โ quizzes, assignments, grading โ Assessment DB
- Discussion Service โ forums and Q&A โ Discussion DB
- Notification Service โ email/push, subscribes to events โ its own DB
- Payment Service โ billing and transactions โ Payment DB
- Analytics Service โ reporting, subscribes to events โ Analytics DB
When a user enrolls, the Enrollment service verifies the user (User API), confirms the course (Catalog API), takes payment (Payment API), records the enrollment, then publishes a UserEnrolled event โ which Notification and Analytics consume independently. Notice enrollment is synchronous (the user waits), while notifications and analytics are asynchronous (they can happen after).
๐๏ธ Exercise 2: Spot the shared-database smell
Goal: A teammate proposes that the Notification service read the orders table directly to know when to send a "your order shipped" email, "to avoid an extra API call." Explain in two or three sentences why this breaks microservices principles, and propose a better design.
โ Solution
Reading the Order service's table directly couples the two services at the schema level: any change to the orders table can silently break Notification, and neither can be deployed independently anymore โ the exact benefit microservices exist to provide. It also means two services now believe they understand "an order," so business rules leak. The better design is event-driven: the Order service publishes an OrderShipped event, and the Notification service subscribes and reacts. Order stays the sole owner of its data, and the two services stay independently deployable.
๐ฏ Quick Quiz
Question 1: What is the single defining property that separates true microservices from a "distributed monolith"?
Question 2: The Order service needs a user's email. What should it do?
Question 3: For a brand-new startup still figuring out its product, what does the lesson recommend?
Best Practices & Pitfalls
โ Do
- Start with a modular monolith; extract services once boundaries are stable and the pain is real
- Draw service boundaries around bounded contexts and business capabilities
- Give every service its own private database โ no exceptions
- Version your APIs (
/api/v1/...) so you can evolve without breaking callers - Invest in CI/CD, monitoring, and distributed tracing before you have many services
โ Don't
- Start a greenfield project with microservices "to be safe"
- Let two services share a database or read each other's tables
- Split by technical layer (a "database service," a "logic service") instead of by capability
- Make everything a synchronous call โ that creates tight coupling and cascading failures
- Adopt five languages just because you can; each one is an operational tax
โ ๏ธ The complexity is real, and it's mostly operational
Moving from a monolith doesn't remove complexity โ it moves it from your code into your infrastructure. You trade a tricky codebase for network calls, service discovery, distributed transactions, and dozens of things to monitor. That's a worthwhile trade at scale and a crushing one too early. Adopt the tooling and the discipline, or don't adopt microservices.
Summary
๐ Key Takeaways
- A microservice is an independently deployable service that owns one business capability end-to-end
- Monolith vs. microservices is a trade-off: easy-to-start vs. easy-to-scale โ pick for your actual problems
- Database per service is non-negotiable; sharing a database recouples everything
- Draw boundaries around bounded contexts, not technical layers
- Don't start with microservices โ go monolith-first and extract gradually
- Beware the distributed monolith: tightly coupled services with all the cost and none of the benefit
๐ Additional Resources
- microservices.io โ Chris Richardson's pattern catalog
- Martin Fowler โ Microservices (the foundational article)
- Martin Fowler โ Bounded Context
- Martin Fowler โ MonolithFirst
๐ What's Next?
You can now decompose a system into independently deployable services. But services are useless if they can't talk to each other reliably. Next up: Service Communication Patterns โ synchronous REST and gRPC, asynchronous events and message queues, service discovery, and the resilience patterns (timeouts, retries, circuit breakers, sagas) that keep a distributed system standing when parts of it fail.
๐ Great work!
You understand the shape of modern distributed systems โ and, just as importantly, when to reach for them and when to walk away.