The architectural style behind modern distributed systems.

About this module

Before deploying anything to AWS, you need to understand what microservices really are — not as a buzzword, but as an architectural decision with pros, cons, and concrete principles. Here we'll formally design the three services of the project.

Level: Intermediate · Estimated time: 3-4 hours


Table of Contents

  1. Why do microservices exist?
  2. What defines a microservice
  3. Fundamental principles
  4. Communication between services
  5. The real trade-offs
  6. Designing our system
  7. API contracts
  8. Data strategy
  9. Practice exercises
  10. Next steps

01. Why do microservices exist?

Before learning any technique, it's important to understand the problem it solves. Microservices aren't a fad — they emerged as a concrete answer to the limitations of the monolithic model in systems that grew too large.

The monolith and its limits

In a monolith, all of an application's code lives in a single process, with a single database and a single deployment. For small and medium projects, this is great: simple to develop, simple to test, simple to operate.

The problem appears when the system grows. Larger teams start stepping on each other's toes, deployments become risky (everything ships together), a failure in one part of the code can bring down the entire application, and scaling one specific feature means scaling the whole monolith — a waste of resources.

The microservices proposal

The core idea is simple: break the monolith into small, independent services, each with a clear responsibility, its own database, and an autonomous deployment cycle. Communication between them happens via APIs or messaging.

Each service can be developed by a different team, scaled independently, written in a different language if it makes sense, and — perhaps most importantly — fail in isolation without bringing down the whole system.

Why study microservices now?

Regardless of whether you use microservices day to day, the principles behind them — separation of concerns, well-defined contracts, asynchronous communication, idempotency — are fundamental to any modern system. Learning microservices is learning to think in distributed systems.

02. What defines a microservice

There's a lot of confusion about what is and isn't a microservice. Having three separate Node.js applications on the same database is not a microservices architecture — it's a distributed monolith, which combines the worst of both worlds.

The essential characteristics

  • Single responsibility — Each service covers a single business domain. Orders handles orders, Inventory handles stock. There's no overlap.
  • Private database — Each service owns its data. Other services don't access the database directly — only via API or events.
  • Independent deployment — Shipping a new version of Orders doesn't require touching Inventory. Release cycles are fully decoupled.
  • Isolated failure — If Payments goes down, Orders keeps accepting orders (they stay pending). A service being offline doesn't bring down the whole system.
  • Independent stack — Each service can use the language, framework, and database that best fit its domain. (In our project we'll standardize on Node.js to keep things simple, but that's a conscious choice.)

Service size: how “micro”?

There's no fixed rule about how many lines of code or how many endpoints define a microservice. The useful metric is the business context (the Bounded Context, from Domain-Driven Design): a service should cover everything that belongs to the same domain and nothing that belongs to another.

Orders have specific rules: creation, cancellation, status, history. All of that is Orders. Inventory has other rules: inflows, outflows, reservations, low-stock alerts. All of that is Inventory. Mixing the two into a single service would be going back to the confusion of the monolith.

03. Fundamental principles

The principles below are the ones that will most guide your decisions as you build the project. Reread this section whenever you're unsure about how to divide responsibilities.

Single Responsibility

A service should have one reason to change. If you need to modify Orders both when the order rule changes and when the payment rule changes, the responsibilities are mixed. Separate them.

Database per Service

Each service has its own database. This feels strange at first (and creates data duplication in some cases), but it's what guarantees real independence. Sharing a database between services creates invisible coupling: a schema change affects multiple services, and it's never clear who owns each table.

Smart Endpoints, Dumb Pipes

The intelligence lives in the services, not in the communication channel. Use simple messaging (SNS/SQS, HTTP) and keep all business logic inside the services. Avoid complex ESBs and flows that hide logic in pipeline configuration.

Decentralized Governance

Don't try to standardize everything. Two services can use different databases (one SQL, another NoSQL) if that makes sense for their domains. Excessive standardization is a remnant of monolithic thinking.

Design for Failure

Distributed systems fail. Always. Networks fluctuate, services go offline, messages get lost. You need to design for failure: timeouts, retries, circuit breakers, dead letter queues, idempotency.

In our project, we'll apply these patterns in practice. If Payments is offline, Orders still accepts orders (status = pending_payment) and the system recovers when the service comes back.

Idempotency

An operation is idempotent when running it more than once has the same effect as running it once. In distributed systems, this is essential: the same message may be delivered twice (due to a transient failure), and your service needs to handle that without corrupting data.

Example: creating an order with the same idempotency_key twice should create the order only once. The second call should return the already-existing order, not create a duplicate.

04. Communication between services

How services talk to each other is one of the most important decisions in the architecture. There are two broad categories, with very different characteristics.

Synchronous: direct call (HTTP/REST)

Service A calls service B and waits for the response. It's the most intuitive model, similar to calling a function. It works well when A needs B's data before it can continue.

  • Advantages: simple, straightforward to debug, clear semantics.
  • Disadvantages: temporal coupling (A only works if B is online), cumulative latency, failure propagation.

Asynchronous: messaging (SNS/SQS, Kafka)

Service A publishes an event and moves on with its life. Other interested services consume the event when they can. There's no waiting, no temporal coupling.

  • Advantages: decoupled services, resilient to failures, natural scalability.
  • Disadvantages: more complex debugging, eventual consistency, event ordering not guaranteed (in general).

When to use each one?

It's not about “picking one and always using it.” You use both at different moments:

ScenarioRecommended typeWhy?
Customer creates orderSynchronous (HTTP)Customer needs an immediate response.
Notify inventoryAsynchronous (event)Inventory doesn't need to be updated in real time.
Validate userSynchronous (HTTP)The decision depends on the response.
Send emailAsynchronous (event)Can be done in the background.
Check balanceSynchronous (HTTP)Real-time data is required.
Action auditingAsynchronous (event)Doesn't block the main flow.

Eventual consistency

When you use events, you lose strong consistency. If an order is created in Orders, it will take a few milliseconds (or seconds) until Inventory knows about it. During that interval, the system is in an inconsistent state — and that's fine, as long as you design for it.

05. The real trade-offs

Microservices are not “the natural evolution” of the monolith. They're a choice that brings new problems in exchange for solving others. Knowing those problems is what separates people who use microservices well from those who are just following a trend.

What you gain

  • Granular scalability — scale only the service that needs it.
  • Independent teams — each team owns its service, with its own release cycles.
  • Resilience — isolated failures instead of cascades.
  • Technological freedom — choose the right stack for each problem.
  • Maintainability — smaller codebases are easier to understand and refactor.

What you lose (or complicate)

  • Operational complexity — now you have N deployments, N pipelines, N monitoring points.
  • Distributed debugging — a request passes through several services; logs need to be correlated.
  • Eventual consistency — you have to deal with temporarily inconsistent states.
  • Latency — network calls between services are orders of magnitude slower than in-process calls.
  • Distributed transactions — you can't do a simple ROLLBACK; you need Sagas or compensations.
  • Infrastructure cost — more services, more machines, more $$.

The 80/20 rule

In real projects, microservices usually make sense in 20% of cases that face problems monoliths handle poorly: very large teams, the need for feature-specific scaling, or severe isolation requirements. For the other 80%, a modular monolith is better.

In our project, we'll use microservices because the goal is to learn — not because the system would be large enough to justify them in production. It's important to be clear about that distinction.

06. Designing our system

Let's formalize the design of the project's three services. This is the blueprint that will guide all the work in the coming modules.

Identifying the domains

Before the code, before AWS, before anything: what are the business domains? In our simplified e-commerce, three main domains emerge:

  • Orders — everything about an order's lifecycle (creation, status, cancellation, history).
  • Inventory — everything about product stock (availability, reservations, decrements).
  • Payments — everything about charging (processing, refunds, payment status).

Interaction map

How do these three services connect to form the flow of an order?

Renderizando diagrama…

Figure 1 — Interaction map between the three services.

Why these choices?

Customer → Orders is synchronous HTTP because the customer needs the orderId immediately to show on screen. Orders → Payments is synchronous HTTP because the order status depends on the payment result. Orders → Inventory is asynchronous via event because the stock decrement can happen a few seconds late without harming the customer.

07. API contracts

In microservices, the API contract is the only agreement between teams. Contract changes break clients — so this part requires care and rigorous documentation.

API design principles

  • Resources as nouns: /orders, not /getOrders.
  • Semantic HTTP verbs: GET to read, POST to create, PUT/PATCH to update, DELETE to remove.
  • Correct status codes: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 422 Unprocessable Entity, 500 Internal Server Error.
  • Explicit versioning: /v1/orders, to allow the contract to evolve without breaking existing clients.
  • Idempotency: creation POSTs accept an Idempotency-Key header.
  • Pagination: never return a list without paginating; use cursors or offsets.

Orders Service — endpoints

MethodPathDescription
POST/v1/ordersCreates a new order
GET/v1/orders/{id}Returns an order by ID
GET/v1/ordersLists orders (paginated)
PATCH/v1/orders/{id}/cancelCancels an order
GET/v1/orders/{id}/statusQueries current status

Inventory Service — endpoints

MethodPathDescription
GET/v1/products/{id}Product details and stock
GET/v1/productsList of products (paginated)
POST/v1/products/{id}/restockRestocks inventory
GET/v1/products/{id}/availabilityChecks availability

Payments Service — endpoints

MethodPathDescription
POST/v1/paymentsStarts a payment
GET/v1/payments/{id}Queries payment status
POST/v1/payments/{id}/refundRequests a refund

Published events (asynchronous)

Beyond the REST APIs, services communicate via events. Each service is responsible for publishing the events of its own domain:

EventPublished byConsumed by
order.createdOrdersInventory, Notifications
order.cancelledOrdersInventory, Payments
order.paidOrdersNotifications
inventory.depletedInventoryOrders
payment.failedPaymentsOrders, Notifications

08. Data strategy

Each service has its own database. But how do you decide which database to use for each one? And how do you deal with the inevitable data duplication between services?

Database per service, not per table

The rule is strong: each service, its own database. Even if all three use DynamoDB (as we will in the project), they are tables in different accounts/scopes, with separate IAM permissions. Orders has no direct access to the Inventory table, even if the infrastructure would allow it.

Data duplication is normal

In microservices, it's common (and desirable) for data to be duplicated. For example: an order in Orders stores productName and price at the moment of purchase, even though that data also exists in Inventory. Why?

  • History: if the product's price changes tomorrow, today's order still shows today's price.
  • Independence: Orders can display order details even if Inventory is offline.
  • Performance: no need to make a network call to another service on every listing.

How do you keep data in sync?

Synchronization happens via events. When a product is registered in Inventory, a product.created event is published. Other interested services consume that event and update their own copies.

This is eventual consistency in action: for a few milliseconds, the data may be stale in Orders. For most business cases, that's acceptable. For the ones that aren't, you use a synchronous call.

Why DynamoDB for the project?

We'll use DynamoDB in all three services for a few pedagogical and practical reasons:

  • It's in the permanent Free Tier (25 GB of storage, 25 RCUs/WCUs).
  • It's serverless: there's no instance to manage, it scales automatically.
  • NoSQL modeling forces you to think in access patterns, which is an essential skill.
  • It integrates natively with Lambda (DynamoDB Streams) — we'll use this later.
  • Single-digit millisecond latency regardless of scale.

09. Practice exercises

The exercises in this module are mostly on paper — modeling, design, and reflection. You won't touch AWS yet. It's part of the architect's job: think before you build.

Exercício
01

Identify microservice violations

Imagine three scenarios and say which of them breaks microservices principles, and why: (a) two services that read from the same users table; (b) an Orders service that calls Payments via HTTP; (c) a team needs to do a coordinated deployment of Orders and Inventory every time the order schema changes. Write your analysis in text.

Exercício
02

Design the cancellation flow

Detail, in text and with arrows, what happens when a customer cancels an order. Who calls whom? Which events are published? Which services consume each event? Consider error cases: what if Payments is offline at the moment of the refund?

Exercício
03

Add a fourth service

Suppose we need to add a Notifications Service, responsible for sending SMS, email, and push notifications to customers. Model it: which events does it consume? Does it have its own APIs? What data does it need to duplicate from other services? Where does it fit in the current diagram?

Exercício
04

Idempotency in practice

Think about the POST /v1/orders endpoint. If the customer makes the same call twice (for example, a retry after a timeout), you can't create two orders. Describe the solution using an Idempotency-Key: what the client sends, what the server stores, how it recognizes the duplicate.

Exercício
05

Written reflection

In a free-form text (10 to 15 lines), answer: “Why is it tempting to share a database between microservices, and why is that temptation wrong?” Use concrete examples from our project.

10. Next steps

You've finished the densest conceptual part of the course. Starting with the next module, we'll get off paper and start working with AWS.

What you learned

  • Why microservices exist and which problems they solve.
  • The essential characteristics that distinguish microservices from a “distributed monolith.”
  • Fundamental principles: SRP, database per service, idempotency, design for failure.
  • Synchronous vs. asynchronous communication — when to use each.
  • The real trade-offs of adopting this architecture.
  • The formal design of the project's three services and their API contracts.
  • The data strategy: controlled duplication, synchronization via events.

What's coming in Module 02

DynamoDB and data modeling. We'll go down to the level of concrete structure: how to model the Orders, Inventory, and Payments tables, choose partition keys, define GSIs (Global Secondary Indexes) for the access patterns we need, and finally create the first tables in the AWS console.

Good design is invisible. Enjoy the journey — Module 02 is up next.