Marta has made the decision, and Luis has been tasked with carrying it out. Before drawing a single service, the team needs a shared set of design principles: simple rules for judging every decision ("is this boundary a good one?", "is this call in the right place?", "is this service too small?"). Without explicit principles, each developer will design according to their own intuition, and the result will be the distributed monolith that lesson 01-04 already warned us about.
This lesson presents the ten principles that will guide all of module 2 and, really, the whole course: cohesion and coupling, single responsibility, autonomy, the contract as the only door, decentralized data, design for failure, appropriate size, Conway's Law, smart endpoints, dumb pipes, and automation. To keep them from remaining theory, each principle is applied to TechCorp's monolith and, at the end, we return to the six problems we flagged in createOrder in lesson 01-05 to classify them by the principle they violate. The decomposition itself (which services come out of the monolith) is the subject of 02-02, and defining their boundaries is the subject of 02-03: here we set the criteria we will judge them by.
Contents
- Why start with principles rather than boxes
- High cohesion and low coupling: the four kinds of coupling
- Single responsibility and modeling around business capabilities
- Autonomy and independent deployment
- The contract as the only door: hiding internal details
- Decentralized data (statement)
- Design for failure (statement)
- Appropriate size: "micro" does not mean tiny
- Conway's Law applied to TechCorp's teams
- Smart endpoints, dumb pipes
- Automation as a design principle
- An X-ray of
createOrder: which principle each problem breaks
- Why start with principles rather than boxes
When a team decides to "do microservices", the immediate temptation is to grab a whiteboard and draw boxes. The problem is that the boxes draw themselves: six areas in the monolith, six services, done. What does not draw itself is the quality of the boundaries between those boxes, and that is where a distributed system succeeds or fails.
Principles serve three purposes:
- Deciding with a rationale when the domain is not obvious. At TechCorp, does stock reservation belong to Inventory or to Orders? Do shipping addresses belong to Customers or to Orders? Principles give a reasoned answer; intuition does not.
- Reviewing other people's designs with a shared vocabulary. When Luis reviews the Payments team's design, he wants to be able to say "this introduces temporal coupling", not "I don't like this".
- Detecting drift over time. Systems degrade through the accumulation of small exceptions ("just this once I'll read another service's table"). Principles are the baseline against which drift is measured.
The principles that follow are not an invention of this course: they come from the work of Sam Newman, Martin Fowler, James Lewis, Chris Richardson and the DDD community. What we do here is organize them and ground them in TechCorp's store.
- High cohesion and low coupling: the four kinds of coupling
This is the parent principle; all the others are ways of achieving it.
- Cohesion: things that change together should live together. If every time a business rule changes you have to touch three services, cohesion is low.
- Coupling: the degree to which a change in one service forces a change in (or affects the operation of) another. We want it to be the bare minimum.
The classic phrase is "changing one thing should require touching a single service and deploying only that service". In TechCorp's monolith the opposite happens: when Notifications changed a query on orders, it broke an Orders report (problem 3 in 01-05).
Coupling is not a single thing. Distinguishing its kinds is what makes it possible to attack it:
| Kind of coupling | What it means | How it shows up at TechCorp today | How it is reduced |
|---|---|---|---|
| Temporal | A needs B to be available at the very same moment to complete its work. | createOrder does not respond if the payment provider or the mail server does not respond. The email provider outage took down charges. |
Asynchronous, event-based communication where the business allows it (03-02); timeouts and circuit breakers (06-03). |
| Deployment | To deploy A you have to deploy (or stop) B. | Everything is deployed on Thursday nights; a change in the catalog forces a deployment of orders and payments. | One artifact per service, independent pipelines (05-03), versioned contracts (03-06). |
| Data | A reads or writes B's data structures directly. | createOrder runs SELECT on customers and products and UPDATE on stock and payments; the FKs cross every area. |
Database per service (02-04); references by identifier; read-only copies. |
| Contract | A depends on the shape of B's interface (fields, types, semantics). | Today there is no contract: any module calls any function and reads any column. | It is the only acceptable and unavoidable coupling; it is managed with explicit, versioned contracts (03-06) and contract tests (04-05). |
Two important observations:
- Contract coupling is the good kind. Two services that communicate have to share something: the shape of the message. The design goal is for that to be the only coupling, and for it to be explicit, small and stable.
- Temporal coupling is the most treacherous, because it is invisible in the code: a chain of synchronous HTTP calls looks clean and modular, but if five services call each other in a chain, the availability of the whole is the product of the five (0.99⁵ ≈ 0.95) and the latency is the sum. A schematic example:
// Version with maximum temporal coupling (NOT the course's target design)
// Every await is an availability dependency at the very same moment.
async function createOrderChained(request) {
const customer = await customersApi.get(request.customerId); // if customers goes down, no order
const prices = await catalogApi.prices(request.lines); // if catalog goes down, no order
await inventoryApi.reserve(request.lines); // if inventory goes down, no order
await paymentsApi.charge(customer, prices); // if payments goes down, no order
await notificationsApi.sendConfirmation(customer); // if email goes down... no order either!
}This code "does microservices" and yet reproduces exactly the monolith's defect: an email outage still prevents selling. Notice that the last await is indefensible from the business's point of view: a charged order is an order, whether the email was sent or not. Principles exist precisely to catch this before it gets written.
- Single responsibility and modeling around business capabilities
A microservice should have one reason to change, and that reason should be a business capability: something the company does that a business owner would recognize in a single sentence ("manage the catalog", "charge orders", "notify the customer").
What is not a good unit of responsibility:
| Cutting criterion | Example | Why it fails |
|---|---|---|
| Technical layer | persistence-service, validation-service, api-service |
Every business change cuts across every layer: maximum coupling. |
| Database entity | orders-table-service, lines-table-service |
This is the "entity service" from 01-04: a CRUD with no behavior; the logic ends up scattered among the callers. |
| Isolated verb | compute-total-service |
Nanoservice: too small to have real autonomy, too chatty with the others. |
| Business capability | inventory-service: knows the stock, reserves it, releases it, records receipts |
Changes when the warehouse rules change, and only then. |
At TechCorp, the six functional areas from 01-05 (customers, catalog, inventory, orders, payments, notifications) are recognizable business capabilities: each has a different business owner, its own vocabulary and its own pace of change (the catalog changes daily; the charging rules, a few times a year). That is a sign the cut is headed in the right direction, although in 02-03 we will refine the boundaries more rigorously.
A quick test Luis will use: "Can I explain what this service does in one sentence without using the word 'and'?". "Manages stock and reservations" passes (reserving is part of managing stock). "Creates orders and sends emails" does not.
- Autonomy and independent deployment
A service is autonomous when its team can change it, test it and deploy it without asking permission or coordinating with other teams, and when it can keep working (even in a degraded mode) when others are not there.
Autonomy has three faces:
- Code autonomy: a repository or, at the very least, a module with a clear owner and its own pipeline.
- Data autonomy: nobody else reads or writes its storage (principle 6).
- Runtime autonomy: it does not depend on another service being alive to answer the requests it can resolve on its own.
The acid test is independent deployment: if putting catalog-service v2.3 into production requires deploying orders-service at the same time, there is no autonomy; there is a monolith spread over several processes. That is TechCorp's problem 1 (fearful weekly deployments, 4 out of 12 rolled back): the Thursday-night deployment exists because nothing can be deployed separately.
Autonomy does not mean total isolation: services collaborate. It means the collaboration happens through stable contracts (principle 5) and that one service's availability does not drag the others down (principle 7).
- The contract as the only door: hiding internal details
A microservice exposes a contract (an HTTP API, a set of published events, or both) and everything else is private: its tables, its collections, its files, its internal module structure, its technology decisions. It is the system-scale application of the encapsulation you already know from object-oriented programming.
Direct consequences:
- No service accesses another service's database. Not even to read. Not even "just for one report".
- No service imports another's business code (
require('../../catalog-service/models/product')is a violation). - A service can change its internal technology (from PostgreSQL to MongoDB, as the catalog will) without anyone noticing.
- The contract is the only thing that has to be versioned and protected (03-06).
Compare the two ways Orders can learn a product's price:
// TODAY (monolith): Orders reads the Catalog table. Data coupling.
const { rows: products } = await client.query(
'SELECT id, name, price FROM products WHERE id = ANY($1) AND active = true', [ids]);
// TARGET: Orders talks to Catalog ONLY through its contract. Contract coupling.
// The contract (which we will design in 03-01) could be: GET /products?ids=p-501,p-777
// returning [{ productId, name, price, active }]. How Catalog stores it internally
// (table, MongoDB collection, cache) is its own business.
const products = await catalog.getProducts(ids); // HTTP client, not SQLNote the detail: in the second version, orders-service does not know that the catalog has an active column or that a products table exists. It only knows the contract. When the catalog moves to MongoDB in 02-04, this line will not change.
A less obvious consequence: the contract must express business operations, not data operations. POST /reservations ("reserve these quantities for this order") is a business contract; PATCH /stock/{id} with { reserved: 7 } is exposing the table over HTTP, and it forces callers to know the inventory's internal logic (that reserved cannot exceed quantity, for instance).
- Decentralized data (statement)
Each service is the exclusive owner of its data: it decides its schema, its technology and its lifecycle, and it is the only one that writes it. Other services obtain that data through the contract (by querying the API or subscribing to the events the owner publishes), never by reading its storage.
This is the principle that generates the most resistance, because it has a real cost: you lose JOINs across areas and you lose the single transaction. At TechCorp, that is exactly what createOrder does: one transaction that touches customers, products, stock, orders, order_lines and payments. Lesson 02-04 develops the database-per-service pattern, how cross-area foreign keys are broken and what alternatives exist for the queries that are a JOIN today; 02-05 replaces the transaction with a saga. Here it is enough to set the rule: one table, one owner.
- Design for failure (statement)
In a monolith, a function call does not fail because of the network. In a distributed system, every interaction between services can fail, take longer than expected or run twice. The eight fallacies of distributed computing from 01-02 are the list of what cannot be assumed.
Designing for failure means, from the design phase onward:
- Deciding what each service does when its dependency does not respond: does it fail? Does it answer with cached data? Does it queue the work for later?
- Assuming messages can arrive duplicated and designing idempotent operations.
- Setting timeouts on every outgoing call and never leaving a request waiting indefinitely.
- Preferring partial degradation to total failure: if Notifications goes down, orders must keep coming in.
The concrete mechanisms (retries with backoff, circuit breakers, bulkheads, dead-letter queues) are studied in 06-03. In this module, the principle translates into design decisions: which interactions are synchronous and which asynchronous, and what compensations exist when a step in a flow fails (02-05).
- Appropriate size: "micro" does not mean tiny
The "micro" prefix has done a lot of damage. There is no number of lines of code, tables or endpoints that defines the right size. The useful criteria are different:
| Criterion | Statement | Application to TechCorp |
|---|---|---|
| Two-pizza team (Amazon) | A service must be fully maintainable by a team that can be fed with two pizzas (5-8 people). | With ~25 engineers, TechCorp has enough for 3-5 teams, not for 20 services. Six services is a reasonable number. |
| Fits in your head | A new developer must be able to understand the whole service (code, data, contract) in a few days. | The monolith fits in nobody's head; inventory-service (stock, reservations, receipts) does. |
| Rewritable in two weeks | If it had to be redone from scratch, a team should be able to do it in a couple of sprints. | Forces services to stay small but does not force fragmentation. |
| One business capability | Neither half a capability nor two. | Already covered in principle 3. |
| Low coordination cost | If a typical business change touches more than two services, the cut is too fine. | Adding a discount coupon (the case from 01-03) touches orders and, at most, a future promotions service. Fine. |
The practical rule: start with a few larger services and split when there is a reason (different scaling, different teams, different pace of change). It is far cheaper to split a service than to merge two that were designed separately with their own contracts and databases. That is why TechCorp will not have a reservations-service separate from inventory-service, nor an order-lines-service separate from orders-service.
- Conway's Law applied to TechCorp's teams
In 1967, Melvin Conway stated that organizations design systems that mirror their communication structure. If three teams build a compiler, you get a three-pass compiler. Applied to microservices: if teams are organized by technical layer (front, back, DB), you get services by technical layer; if they are organized by business capability, you get services by business capability.
The practical consequence is the inverse Conway maneuver: decide first the architecture you want, and organize the teams so that they mirror it. At TechCorp today there is a single development team informally spread across areas, all on the same repository; the 20% of coordination time Luis complains about is Conway's Law in action.
Marta's proposal to accompany the migration:
flowchart LR
subgraph EQ1[Shopping Experience team]
CAT[catalog-service]
CUS[customers-service]
end
subgraph EQ2[Orders team - Luis]
ORD[orders-service]
INV[inventory-service]
end
subgraph EQ3[Payments & Communications team]
PAY[payments-service]
NOT[notifications-service]
end
subgraph EQP[Platform team]
GW[API Gateway]
K8S[Kubernetes, CI/CD, observability]
end
EQ1 -. contracts .- EQ2
EQ2 -. events .- EQ3
EQP -. tooling .- EQ1
EQP -. tooling .- EQ2
EQP -. tooling .- EQ3
Notice two decisions:
- No service has two owning teams and no team has more services than it can maintain. A service with two owners ends up with two design criteria; a team with six services ends up neglecting four.
- There is a platform team that owns no business, but rather the shared tooling (cluster, pipelines, observability). That is how automation (principle 11) avoids falling on each team separately.
Orders and Inventory sharing a team does not mean they share a database or code: principles 5 and 6 apply just the same within a team. What they share is pace and context: they are the two pieces that talk the most in the order flow, and it pays for their design conversations to be cheap.
- Smart endpoints, dumb pipes
This principle, formulated by Fowler and Lewis, is a reaction to the SOA era we reviewed in 01-01: back then, the Enterprise Service Bus (the "pipe") concentrated business logic: transformations, content-based routing, complex orchestrations. The result was a smart bus surrounded by dumb services, and the bus became a central monolith nobody dared to touch.
In microservices this is inverted:
- The endpoints are smart: business logic lives in the services.
inventory-servicedecides whether it can reserve; nobody decides for it. - The pipes are dumb: HTTP and the message broker (RabbitMQ in this course) transport bytes. They route, deliver, retry, but they do not know what an order is.
What this implies for TechCorp's design:
| Allowed in the "pipe" | Not allowed in the "pipe" |
|---|---|
Routing POST /orders to orders-service (API Gateway, 03-04). |
Having the gateway compute the order total or validate stock. |
Delivering order.created to whoever is subscribed (RabbitMQ). |
Having the broker transform order.created into an "inventory format" and a "payments format". |
| Authenticating the JWT and applying rate limits (gateway). | Having the gateway decide which customers get a discount. |
| Retrying the delivery of a message. | Having the broker decide whether a rejected payment should be retried or the order cancelled. |
When the service mesh (Istio) appears in 05-05, we will see that it too respects this principle: the mesh handles networking, security and observability, never business logic.
- Automation as a design principle
It may be surprising that automation is a design principle rather than an operations one. The reason is that a design with six, twelve or twenty services is unworkable without automation, and so automation shapes the design from day one:
- If deployment is not automatic, nobody will deploy services independently and it will be back to "Thursday night".
- If contract tests are not automatic, nobody will dare to change a contract.
- If creating a new service is not automated (template, pipeline, metrics dashboard), the cost of creating a service will be so high that everything will get crammed into the existing ones.
- If there is no automatic observability, a flow spread over five services will be impossible to debug.
That is why TechCorp's design includes, as part of the system rather than as an add-on, GitHub Actions (05-03), Docker and Kubernetes (05-01, 05-02), Prometheus/Grafana/Loki (06-01) and OpenTelemetry (06-02). And that is why the platform team from section 9 exists. Luis's rule: "If it has to be done more than once per service, automate it before creating the second service".
- An X-ray of
createOrder: which principle each problem breaks
createOrder: which principle each problem breaksIn 01-05 we flagged six problems in the createOrder code. Now we can classify them precisely, and that classification is the script for the lessons to come:
| # | Problem flagged in 01-05 | Principle(s) violated | Kind of coupling | Where it is solved |
|---|---|---|---|---|
| 1 | One function does six things from five different areas. | Single responsibility / business capabilities; appropriate size. | — (low cohesion) | 02-02 (decomposition), 02-03 (boundaries). |
| 2 | Direct access to other areas' tables (customers, products, stock, payments). |
Contract as the only door; decentralized data. | Data. | 02-04 (DB per service). |
| 3 | A single transaction protects everything. | Decentralized data; autonomy. | Data and temporal (every area must be available and locked at the same time). | 02-05 (sagas). |
| 4 | External call (payment provider) inside the transaction. | Design for failure; autonomy. | Temporal. | 02-05 (the saga moves the charge into its own step), 06-03 (timeouts, circuit breakers). |
| 5 | Synchronous email sending. | Design for failure; low temporal coupling; smart endpoints, dumb pipes (Notifications should react to an event, not be invoked). | Temporal. | 02-05 (order.confirmed event), 03-02 (messaging). |
| 6 | No idempotency. | Design for failure. | — | 02-05 (idempotency keys), 06-03. |
And there is a seventh problem we did not flag back then because we lacked the vocabulary: deployment. createOrder lives in the same artifact as the catalog search, so a change in how search results are sorted forces a redeployment of the charging logic. That is pure deployment coupling, it violates autonomy, and it is what the module 5 lessons solve once the design allows it.
This table is module 2's "contract" with the reader: by the end of 02-05, every row will have a designed solution.
Common Mistakes and Tips
- Confusing "principle" with "absolute rule". Principles are weighed. Sometimes temporal coupling is accepted (a synchronous query to Catalog to display prices) because the alternative (replicating the whole catalog) is worse. What matters is that the exception is conscious and documented.
- Aiming for "small microservices" instead of "autonomous microservices". Size is a consequence, not a goal. A 30,000-line service that one team deploys ten times a day without coordinating with anyone is a good microservice.
- Designing services and leaving the teams as they were. Conway's Law always wins. If a single team maintains six services in six repositories, it will end up deploying them together "to keep things simple".
- Exposing the database over HTTP. A
GET /stock/{id}that returns the row as-is and aPATCH /stock/{id}to modify it is not a business contract; it is data coupling in disguise. Contracts talk about reservations, charges and orders. - Leaving automation "for later". Later never comes, and without it the second service already hurts.
- Tip: write the principles on a page of the team wiki, with one TechCorp example per principle, and use them as a checklist in every design review. That is what Luis will do starting this week.
Exercises
Exercise 1: Classifying coupling
For each situation in TechCorp's monolith, indicate which kind(s) of coupling it describes (temporal, deployment, data, contract) and which principle is being violated:
- When Orders added a column to
order_lines, the migration failed because of a lock held by a heavy Catalog query. - A change to the confirmation email template requires running the full 40-minute suite and deploying on Thursday.
- If the payment provider takes 8 seconds to respond, the
POST /ordersrequest takes 8 seconds and thestockrows stay locked in the meantime. - The reporting module directly imports
controllers/ordersController.jsto reuse a function that computes totals.
Exercise 2: The one-sentence test
Apply the "can I describe it in one sentence without 'and'?" test to these service candidates and decide whether each is a valid business capability, a service that is too big, or a nanoservice. Justify in one line:
vat-calculation-serviceinventory-service(stock, reservations, releases, warehouse receipts)orders-and-billing-servicenotifications-service(emails and SMS triggered by events)
Exercise 3: Conway in reverse
Marta proposes an alternative organization: a "Back-end" team (all the services), a "Front-end" team and a "Data" team (all the databases). Explain in 4-6 lines what architecture Conway's Law predicts for that organization and why it is incompatible with principles 4, 5 and 6.
Solutions
Exercise 1
- Data coupling (two areas on the same physical schema) with a deployment effect (one area's migration blocks the other). Violates decentralized data and autonomy.
- Deployment coupling: a trivial change in one capability drags along the deployment of all of them. Violates autonomy and independent deployment; underneath, automation (a 40-minute suite does not allow frequent deployments).
- Temporal coupling with the payment provider, aggravated by data coupling (locking
stock, which belongs to another area). Violates design for failure and decentralized data. - Implicit, unmanaged contract coupling (it depends on the internal signature of a function that is not a public contract) and, at bottom, it violates the contract as the only door: reports should consume an API or an event, not another area's internal code.
Exercise 2
vat-calculation-service: nanoservice. It is a function, not a capability; nobody in the business would say "our capability to calculate VAT". It should be a library or part of Orders/Payments.inventory-service: valid capability. "Manages stock" (reserving and releasing are stock operations). A warehouse manager recognizes it.orders-and-billing-service: too big or, at the very least, two capabilities with different paces and owners (sales vs. accounting). A candidate for splitting as soon as there is a reason (for example, tax requirements that change without touching the order lifecycle).notifications-service: valid capability. "Tells the customer what is happening" is a sentence without "and" (email and SMS are channels of the same capability, not different capabilities).
Exercise 3
Conway's Law predicts a layered architecture: a big "back-end" (which would tend to be a single service, or a set of services deployed together because a single team maintains them), a separate front-end, and a shared database managed by the Data team, with schemas designed by that team for everyone. It is incompatible with autonomy (no team can deploy a business capability end to end without coordinating with the other two), with the contract as the only door (the Data team would access everything and services would share tables by design) and with decentralized data (there would be a single owner of all the databases, i.e., the shared DB from 01-01: a distributed monolith). To get autonomous microservices, teams must be vertical, by business capability, with a horizontal platform team only for tooling.
Conclusion
We have set the criteria by which we will judge everything we design: high cohesion and low coupling (distinguishing temporal, deployment, data and contract coupling), single responsibility around business capabilities, autonomy with independent deployment, the contract as the only door, decentralized data, design for failure, appropriate size (two-pizza teams, "fits in your head"), Conway's Law applied to TechCorp's four teams, smart endpoints, dumb pipes and automation from the design stage. And we have gone back to createOrder to classify its six problems (plus the deployment one) by the principle they break: low cohesion, data coupling, single transaction, temporal coupling with the payment provider and with email, and lack of idempotency.
With these principles as a checklist, the next lesson tackles the first big practical question: how the monolith is decomposed. We will look at the strategies (by business capability, by subdomain, by use case, by team), the techniques for discovering the seams (including the coupling analysis of the SQL schema from 01-05), the techniques for safe extraction and, above all, the order in which TechCorp will extract its six services and why the catalog goes first.
Microservices Course
Module 1: Introduction to Microservices
- Basic Concepts of Microservices
- Advantages and Disadvantages of Microservices
- Comparison with the Monolithic Architecture
- When to Adopt Microservices: Decision Criteria
- The Course Case Study: TechCorp's Online Store
Module 2: Microservice Design
- Microservice Design Principles
- Decomposing Monolithic Applications
- Defining Bounded Contexts
- Data Management: One Database per Service
- Distributed Consistency: Sagas, CQRS and Event Sourcing
Module 3: Communication between Microservices
- RESTful APIs
- Asynchronous Messaging
- Communication Protocols: gRPC, GraphQL
- API Gateway and Backend for Frontend
- Service Discovery and Load Balancing
- API Contracts and Versioning
Module 4: Implementing Microservices
- Choosing Technologies and Tools
- Building a Simple Microservice
- Configuration Management
- Hands-On Integration: Consuming APIs and Publishing Events
- Testing Microservices: Unit, Integration and Contract Tests
Module 5: Deployment and Orchestration
- Containers and Docker
- Orchestration with Kubernetes
- CI/CD for Microservices
- Deployment Strategies: Rolling, Blue-Green and Canary
- Service Mesh: Istio and Linkerd
Module 6: Monitoring and Maintenance
- Monitoring and Logging
- Distributed Tracing with OpenTelemetry
- Error Handling and Recovery
- Scalability and Performance
- SLOs, Alerts and Incident Management
Module 7: Security in Microservices
- Authentication and Authorization
- Communication Security
- Security Practices
- Container and Kubernetes Security
