2137 words
11 minutes
Microservices (P1): A Terminology Map — Following One Order Through the System

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.

TermIn short
MonolithOne application, one deployment, usually one database. Not bad. Most products should start this way.
Big Ball of MudA monolith left to grow without boundaries: every module calls every other module, fix one thing and three break. This is the scary part.
Modular MonolithStill 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.
MicroservicesEach service deploys independently, owns its data and talks over the network. In exchange, you take on the complexity of a distributed system.
Conway’s LawA 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.
WARNING

Microservices 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.

TermIn short
Domain-Driven Design (DDD)Designing software around the business domain, using the business’s own language.
Ubiquitous LanguageDevelopers 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 ContextA 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.
AggregateA 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 ObjectAn Entity has identity (OrderId). A Value Object only has value (Money, Address): two Money(100, "VND") are the same thing.
Domain Event vs Integration EventA 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 FigCarve 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.

TermIn short.NET
API GatewayThe 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 LimitingCaps the number of requests per time window to protect the system.Microsoft.AspNetCore.RateLimiting
REST vs gRPCREST (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 CompositionA gateway or service calls several services and merges the results, e.g. an “order details” screen needs Order + Payment + Shipping.
JWT / OAuth 2.0The 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.

TermIn short.NET
Synchronous vs AsynchronousSync: 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 BusThe middleman that receives and distributes messages: RabbitMQ, Kafka, Azure Service Bus.MassTransit, Wolverine
Queue vs TopicA 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
BackpressureWhen the receiver can’t keep up, it needs a way to tell the sender to slow down instead of letting the queue grow forever.
WebhookAn 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?

TermIn short
Database per ServiceOther services may not read your DB directly. If they want data, they ask through an API or listen to events.
Distributed Transaction / 2PCA 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 ConsistencyAccept 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 TheoremWhen the network partitions, you must choose between Consistency and Availability. Microservices usually lean toward A + eventual consistency.
SagaA chain of local transactions, each with a compensating transaction to undo it. Out of stock means refund, then cancel the order.
Choreography vs OrchestrationChoreography: services listen to each other’s events and react, with no coordinator. Orchestration: an orchestrator (a saga state machine) issues each step.
Dual WriteThe 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 OutboxThe 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 ConsumerBrokers usually guarantee at-least-once delivery, so a message can arrive twice. The consumer must make sure running twice doesn’t charge twice.
CQRSSeparate 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 SourcingInstead 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();
}
TIP

MassTransit 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 AnyAsync check. The real version needs a unique constraint on ProcessedMessages.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.

TermIn short.NET
TimeoutNever wait forever. Every outbound call needs a timeout.Polly, HttpClient.Timeout
RetryRetry transient errors with exponential backoff + jitter so you don’t hammer a struggling service. Only retry idempotent operations.Polly
Circuit BreakerToo many failures and the breaker “trips”: fail fast for a while instead of calling again. Three states: Closed → Open → Half-Open.Polly
BulkheadPartition resources (connection pools, concurrency) so one slow dependency can’t drain the whole service.Polly
Health CheckEndpoints 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?

TermIn short.NET
Correlation IDAn 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 LoggingLogs from every service flow into one place (Seq, ELK, Loki), searchable by Correlation ID.Serilog
Distributed TracingReconstructs the whole path of a request as spans, showing which step was slow or failed.OpenTelemetry
MetricsNumbers over time: requests/sec, error rate, p95/p99 latency.OpenTelemetry + Prometheus
SLI / SLO / SLAAn 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.
NOTE

Observability 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?

TermIn short.NET / infra
ContainerizationPackage a service with its runtime into an image that runs the same everywhere.Docker, dotnet publish /t:PublishContainer
Container OrchestrationAutomatically runs, scales and restarts containers.Kubernetes
Service Discovery / RegistryServices find each other by name instead of hardcoded IPs.K8s DNS, .NET Aspire
Externalized ConfigurationConfig lives outside the code, changes per environment, no rebuild needed.IOptions<T>, Azure App Configuration
Sidecar / Service MeshA helper container next to each service handles mTLS, retries and metrics. A mesh is the whole network of those sidecars (Istio, Linkerd).
Blue-Green / CanaryBlue-Green: run old and new side by side, then switch all traffic. Canary: shift gradually 5% → 25% → 100%, watching metrics at each step.
Feature ToggleShip the code with the feature turned off, turn it on when ready. Separates deploy from release.Microsoft.FeatureManagement
Contract TestingTests that Order still sends the format Payment expects, without spinning up the whole system.Pact
Backward CompatibilityA 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:

  1. Foundations: this post, and P2: When not to use microservices
  2. Decomposition: DDD & Bounded Context, Database per Service, Strangler Fig & ACL
  3. Communication: API Gateway & BFF with YARP, Messaging, Kafka vs RabbitMQ
  4. Data consistency: Saga, Outbox, Idempotent Consumer, CQRS, Event Sourcing
  5. Resilience: Retry, Timeout, Circuit Breaker, Bulkhead with Polly
  6. Observability: Logging, Tracing, Health checks with OpenTelemetry
  7. Deployment: Service Discovery, Service Mesh, Canary, Contract Testing
NOTE

References 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).

Microservices (P1): A Terminology Map — Following One Order Through the System
https://www.devwithxuan.com/en/posts/microservices-terminology-en/
Author
XuanPD
Published at
2025-09-23
Share:

Subscribe to Newsletter

Get notified when I publish new posts. No spam, unsubscribe anytime.

Comments