Skip to main content

๐Ÿงฉ Microservices Principles

Week 14: Advanced Architecture โ€” course module banner illustration

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.

A monolith bundles every capability into one deployable unit sharing one database; microservices split them into independently deployed services each with its own database Monolith Users Products Orders + Payments one process ยท one deploy Shared DB Microservices Users Products Orders Payments DB DB DB DB
The monolith is one box over one database. Microservices decompose the same capabilities into separately deployed services, each owning its own store.

Neither is "better" in the abstract. They optimize for different things:

DimensionMonolithMicroservices
Getting startedFast โ€” one repo, one deploySlow โ€” pipelines, discovery, infra per service
Calling other codeIn-memory function call (fast, reliable)Network call (latency, can fail)
ScalingScale the whole app togetherScale hot services independently
Deploying a changeRedeploy everythingRedeploy one service
Team autonomyTeams coordinate in one codebaseTeams own services end-to-end
Data consistencyEasy โ€” one ACID databaseHard โ€” eventual consistency, sagas
Debugging a requestOne stack traceDistributed 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.

graph LR A["User Service repo"] --> B["User pipeline: build, test, deploy"] C["Order Service repo"] --> D["Order pipeline: build, test, deploy"] B --> E["User Service in production"] D --> F["Order Service in production"]

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.

graph TD A["User Service"] --> B[("User DB")] C["Product Service"] --> D[("Product DB")] E["Order Service"] --> F[("Order DB")] E -.asks via API.-> A E -.asks via API.-> C

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.

graph TD A["E-commerce Domain"] --> B["User Context"] A --> C["Catalog Context"] A --> D["Order Context"] A --> E["Payment Context"] A --> F["Shipping Context"] B --> B1["User Service"] C --> C1["Product Service"] D --> D1["Order Service"] E --> E1["Payment Service"] F --> F1["Shipping 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:

graph LR A["Well-structured monolith"] --> B["Identify a stable boundary"] B --> C["Extract first service"] C --> D["Run a hybrid system"] D --> E["Extract more as pain justifies it"]

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

๐Ÿš€ 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.