The first time you join a microservices project, the code is rarely what overwhelms you. It’s a meeting like this: “Order service publishes the event through the outbox, the payment consumer has to be idempotent, if it fails the saga compensates, and don’t forget the correlation ID for tracing.” Every word is English, and you still have no idea what was said.
There are plenty of glossaries. The problem is that isolated definitions don’t stick, because you never see what problem each term was invented to fix.
This post goes the other way: we follow one order request, from the moment a user taps “Place order” until the order is confirmed. Every time something can go wrong, the matching term shows up. This is the first post in the Microservices A–Z series. Each group of terms below gets its own deep-dive post later, with .NET code.
The example system
A small online shop, split into 4 services:
- Order Service: creates and manages orders
- Payment Service: charges the customer
- Inventory Service: reserves stock
- Notification Service: sends email/SMS
flowchart LR
C["Client (Web/Mobile)"] --> GW["API Gateway"]
GW --> O["Order Service"]
O --> ODB[("Order DB")]
O -- "OrderCreated" --> MB{{"Message Broker"}}
MB --> P["Payment Service"]
MB --> I["Inventory Service"]
MB --> N["Notification Service"]
P --> PDB[("Payment DB")]
I --> IDB[("Inventory DB")]
The whole series reuses exactly this system.
Stage 0: Why split at all?
Before there were 4 services, the system usually started as one block.
| Term | In short |
|---|---|
| Monolith | One application, one deployment, usually one database. Not bad. Most products should start this way. |
| Big Ball of Mud | A monolith left to grow without boundaries: every module calls every other module, fix one thing and three break. This is the scary part. |
| Modular Monolith | Still deployed as one unit, but split into modules with clear boundaries (each module owns its API and data). The natural stepping stone before splitting into services. |
| Microservices | Each service deploys independently, owns its data and talks over the network. In exchange, you take on the complexity of a distributed system. |
| Conway’s Law | A system’s architecture mirrors the communication structure of the organization. Forcing 4 teams into 1 service, or making 1 team own 12 services, both hurt. |
WARNINGMicroservices solve an organizational and team-scaling problem before they solve a technical one. If your team has 3 people, you almost certainly don’t need them yet. P2 covers this in detail.
Stage 1: Splitting it right — DDD
First question: where is the boundary between Order and Payment? Get it wrong and the two services end up calling each other constantly. That’s a distributed monolith: worse than the monolith you started with.
| Term | In short |
|---|---|
| Domain-Driven Design (DDD) | Designing software around the business domain, using the business’s own language. |
| Ubiquitous Language | Developers and the business share one vocabulary. If the business says “order” and the code says Cart, a misunderstanding bug is only a matter of time. |
| Bounded Context | A boundary inside which a word has exactly one meaning. “Product” in Catalog (description, images) is not “Product” in Inventory (quantity, bin location). Each Bounded Context is usually a candidate for a service. |
| Aggregate | A cluster of objects that must always stay consistent together, changed through a single entry point (the aggregate root). Order + OrderLine is one aggregate. |
| Entity / Value Object | An Entity has identity (OrderId). A Value Object only has value (Money, Address): two Money(100, "VND") are the same thing. |
| Domain Event vs Integration Event | A Domain Event is something that happened inside a service. An Integration Event is something published outward for other services. |
| Anti-Corruption Layer (ACL) | A translation layer between your model and another system’s model (legacy, third party), so their “dirty” model doesn’t leak into your code. |
| Strangler Fig | Carve pieces out of the monolith gradually instead of rewriting from scratch. Requests move to the new service step by step until nobody uses the old part. |
Stage 2: The request comes in — Communication & APIs
The user taps “Place order”. The request leaves the mobile app and enters the system.
| Term | In short | .NET |
|---|---|---|
| API Gateway | The single entry point: routing, auth, rate limiting. Clients don’t need to know how many services sit behind it. | YARP |
| Backend for Frontend (BFF) | One gateway per client type: mobile wants small payloads, the web admin wants more data. | YARP / Minimal API |
| Rate Limiting | Caps the number of requests per time window to protect the system. | Microsoft.AspNetCore.RateLimiting |
| REST vs gRPC | REST (JSON/HTTP) for external clients. gRPC (Protobuf/HTTP2) is fast with a strict schema, a good fit for internal service-to-service calls. | ASP.NET Core, Grpc.AspNetCore |
| API Composition | A gateway or service calls several services and merges the results, e.g. an “order details” screen needs Order + Payment + Shipping. | |
| JWT / OAuth 2.0 | The gateway authenticates the user once. Services behind it only verify the token. | Microsoft.AspNetCore.Authentication.JwtBearer |
Order Service receives the request and saves the order as Pending. Now it has to tell Payment and Inventory.
Stage 3: Telling other services — Messaging
Order could call Payment directly over HTTP. But if Payment is down, the order goes down with it. The more common approach is to fire an event and move on.
| Term | In short | .NET |
|---|---|---|
| Synchronous vs Asynchronous | Sync: call and wait for the result (HTTP/gRPC). Async: send a message and carry on; the receiver handles it when it’s ready. | |
| Message Broker / Event Bus | The middleman that receives and distributes messages: RabbitMQ, Kafka, Azure Service Bus. | MassTransit, Wolverine |
| Queue vs Topic | A queue is point-to-point: each message has one receiver. A topic is pub/sub: every subscriber gets it. OrderCreated is a topic, because Payment, Inventory and Notification all need to hear it. | |
| Dead Letter Queue (DLQ) | Messages that keep failing past the retry limit land here instead of blocking the main queue, waiting for a human. | Built into MassTransit/Wolverine |
| Backpressure | When the receiver can’t keep up, it needs a way to tell the sender to slow down instead of letting the queue grow forever. | |
| Webhook | An external system (e.g. a payment provider) calls back one of your HTTP endpoints when something happens. |
Stage 4: Data is scattered — Consistency
This is the hardest stage, and the one system design interviews dig into the most.
Each service keeps its own database (Database per Service). So “create order + charge + reserve stock” no longer fits in one transaction. What happens if the charge succeeds and then Inventory says the item is out of stock?
| Term | In short |
|---|---|
| Database per Service | Other services may not read your DB directly. If they want data, they ask through an API or listen to events. |
| Distributed Transaction / 2PC | A transaction spanning several databases. Two-Phase Commit can do it, but it’s slow, holds locks, and most modern brokers and cloud databases don’t support it. Rarely used in practice. |
| Eventual Consistency | Accept that data may disagree for a few seconds, as long as it converges. An order showing “Processing” while waiting for Payment is an example. |
| CAP Theorem | When the network partitions, you must choose between Consistency and Availability. Microservices usually lean toward A + eventual consistency. |
| Saga | A chain of local transactions, each with a compensating transaction to undo it. Out of stock means refund, then cancel the order. |
| Choreography vs Orchestration | Choreography: services listen to each other’s events and react, with no coordinator. Orchestration: an orchestrator (a saga state machine) issues each step. |
| Dual Write | The classic bug: save to the DB, then publish the message. If the app crashes in between, the DB has an order nobody knows about. |
| Transactional Outbox | The fix for Dual Write: write the message to an Outbox table in the same transaction as the data, then a background job reads that table and publishes. |
| Idempotent Consumer | Brokers usually guarantee at-least-once delivery, so a message can arrive twice. The consumer must make sure running twice doesn’t charge twice. |
| CQRS | Separate the write model (commands) from the read model (queries). The read side can be a Materialized View built from events, so you don’t have to join across 4 services. |
| Event Sourcing | Instead of storing current state, store the sequence of events that happened and rebuild state from it. Powerful but expensive. Use it only when you truly need audit or time-travel. |
Outbox with EF Core, the core idea is just this:
await using var tx = await db.Database.BeginTransactionAsync(ct);
db.Orders.Add(order);
db.OutboxMessages.Add(OutboxMessage.From(new OrderCreated(order.Id, order.Total)));
await db.SaveChangesAsync(ct); // order and message commit together
await tx.CommitAsync(ct);
// Background worker reads unsent OutboxMessages → publishes to the broker → marks them sent
And on the consumer side, idempotency by remembering which messages were already handled:
public async Task Consume(ConsumeContext<OrderCreated> context)
{
var messageId = context.MessageId!.Value;
if (await db.ProcessedMessages.AnyAsync(m => m.Id == messageId))
return; // already handled, skip
await paymentService.ChargeAsync(context.Message.OrderId, context.Message.Total);
db.ProcessedMessages.Add(new ProcessedMessage(messageId));
await db.SaveChangesAsync();
}
TIPMassTransit and Wolverine both ship Outbox/Inbox support. In a real project, use it instead of rolling your own. The snippets above are only there to show the mechanism. The consumer also has a hole: if two copies of the same message arrive concurrently, both pass the
AnyAsynccheck. The real version needs a unique constraint onProcessedMessages.Id, written in the same transaction as the business change. The Idempotent Consumer post goes into detail.
Stage 5: A service gets slow — Resilience
Payment Service calls an external payment gateway that sometimes takes 30 seconds to respond. Do nothing, and Payment’s threads all get stuck, then Payment dies, then everything calling Payment dies too. That’s a cascading failure.
| Term | In short | .NET |
|---|---|---|
| Timeout | Never wait forever. Every outbound call needs a timeout. | Polly, HttpClient.Timeout |
| Retry | Retry transient errors with exponential backoff + jitter so you don’t hammer a struggling service. Only retry idempotent operations. | Polly |
| Circuit Breaker | Too many failures and the breaker “trips”: fail fast for a while instead of calling again. Three states: Closed → Open → Half-Open. | Polly |
| Bulkhead | Partition resources (connection pools, concurrency) so one slow dependency can’t drain the whole service. | Polly |
| Health Check | Endpoints that tell the orchestrator whether the service is alive (liveness) and ready for traffic (readiness). | AddHealthChecks() |
Since .NET 8, the Microsoft.Extensions.Http.Resilience package bundles these for HttpClient:
builder.Services.AddHttpClient<PaymentGatewayClient>()
.AddStandardResilienceHandler(); // timeout + retry + circuit breaker, built on Polly v8
Stage 6: A bug in production — Observability
A customer reports: “I was charged but my order is still Pending.” That request went through 4 services and a broker. Logs live in 4 different places. How do you find it?
| Term | In short | .NET |
|---|---|---|
| Correlation ID | An ID attached at the gateway and passed through every service and message, so you can group all logs of one flow. | Activity.Current.TraceId |
| Centralized Logging | Logs from every service flow into one place (Seq, ELK, Loki), searchable by Correlation ID. | Serilog |
| Distributed Tracing | Reconstructs the whole path of a request as spans, showing which step was slow or failed. | OpenTelemetry |
| Metrics | Numbers over time: requests/sec, error rate, p95/p99 latency. | OpenTelemetry + Prometheus |
| SLI / SLO / SLA | An SLI is what you measure (p99 latency). An SLO is the internal target (p99 < 300ms). An SLA is the promise to customers, with penalties if broken. |
NOTEObservability isn’t “we have logs.” The real question is: when an incident you’ve never seen before happens, can you answer “what is going on” using only the data the system already emits?
Stage 7: Deploying and operating
Order Service has a new version. How do you deploy it without disrupting the other 3 services?
| Term | In short | .NET / infra |
|---|---|---|
| Containerization | Package a service with its runtime into an image that runs the same everywhere. | Docker, dotnet publish /t:PublishContainer |
| Container Orchestration | Automatically runs, scales and restarts containers. | Kubernetes |
| Service Discovery / Registry | Services find each other by name instead of hardcoded IPs. | K8s DNS, .NET Aspire |
| Externalized Configuration | Config lives outside the code, changes per environment, no rebuild needed. | IOptions<T>, Azure App Configuration |
| Sidecar / Service Mesh | A helper container next to each service handles mTLS, retries and metrics. A mesh is the whole network of those sidecars (Istio, Linkerd). | |
| Blue-Green / Canary | Blue-Green: run old and new side by side, then switch all traffic. Canary: shift gradually 5% → 25% → 100%, watching metrics at each step. | |
| Feature Toggle | Ship the code with the feature turned off, turn it on when ready. Separates deploy from release. | Microsoft.FeatureManagement |
| Contract Testing | Tests that Order still sends the format Payment expects, without spinning up the whole system. | Pact |
| Backward Compatibility | A new version of an event/API must not break old consumers: add fields only, never change meaning or remove fields. |
I’ve written about Feature Flags in .NET in a separate post if you want to read ahead.
The order’s whole journey
sequenceDiagram
participant C as Client
participant GW as API Gateway
participant O as Order Service
participant MB as Broker
participant P as Payment
participant I as Inventory
C->>GW: POST /orders (JWT)
GW->>O: route + Correlation ID
O->>O: Save Order + Outbox (1 transaction)
O-->>C: 202 Accepted (Pending)
O->>MB: OrderCreated (outbox worker)
MB->>P: OrderCreated
P->>P: Idempotent check → charge (retry, circuit breaker)
P->>MB: PaymentSucceeded
MB->>I: PaymentSucceeded
I-->>MB: InventoryFailed (out of stock)
MB->>P: InventoryFailed → Refund (compensate)
MB->>O: InventoryFailed → Order Cancelled
Reread the meeting from the start: “Order service publishes the event through the outbox, the payment consumer has to be idempotent, if it fails the saga compensates, and don’t forget the correlation ID for tracing.” Every phrase now has a place in the diagram above.
Next in the series
The series has 22 posts, following the same stages:
- Foundations: this post, and P2: When not to use microservices
- Decomposition: DDD & Bounded Context, Database per Service, Strangler Fig & ACL
- Communication: API Gateway & BFF with YARP, Messaging, Kafka vs RabbitMQ
- Data consistency: Saga, Outbox, Idempotent Consumer, CQRS, Event Sourcing
- Resilience: Retry, Timeout, Circuit Breaker, Bulkhead with Polly
- Observability: Logging, Tracing, Health checks with OpenTelemetry
- Deployment: Service Discovery, Service Mesh, Canary, Contract Testing
NOTEReferences for this series: microservices.vn (a thorough Vietnamese catalog of terms and patterns), Chris Richardson’s microservices.io, and Building Microservices (Sam Newman, 2nd edition).
Subscribe to Newsletter
Get notified when I publish new posts. No spam, unsubscribe anytime.