Microservices
By the end of this lesson you'll be able to split a system into independent services along bounded contexts, choose synchronous (gRPC/HTTP) or asynchronous (message bus) communication for each link, make calls fault-tolerant with retries and circuit breakers, and reason about data ownership, observability, and Docker packaging — the real-world skills behind distributed .NET systems.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of a food court versus one giant restaurant kitchen. The giant kitchen (a monolith ) does everything under one roof — if the oven breaks, the whole place stops, and you can't add more pizza ovens without rebuilding the kitchen. A food court is a row of independent stalls : the noodle stall, the burger stall, the smoothie bar. Each has its own kitchen, its own till, its own staff — that's a microservice . If the smoothie blender dies, you still get your noodles. Busy stalls add staff without touching the others. They cooperate by passing orders (messages and API calls), never by reaching into each other's fridges (each service owns its own data). That independence is the whole point — and the source of the extra complexity you'll learn to manage here.
Microservices are not "better" — they trade simplicity for independence . Reach for them when you have real scaling pressure or multiple teams. Otherwise a well-structured monolith is often the right call.
1. Service Boundaries — One Service per Bounded Context
The first and hardest decision is where to cut . Draw each boundary around a bounded context — a self-contained slice of the business with its own language and rules, like Orders, Inventory, or Shipping. A good boundary means a service can do its job using mostly its own data and change without forcing changes elsewhere. Crucially, a service owns its data: others see a contract (a message or an API response), never the raw tables. Read this worked example, run it, and notice how the services cooperate by passing a small record rather than sharing storage.
2. Communication — Synchronous vs Asynchronous
Services talk in two fundamentally different ways. Synchronous calls (gRPC or HTTP) are a phone call: you ask, you wait, you get an answer right now — great when you genuinely need the reply to continue, but the caller is blocked and coupled to the callee being up. Asynchronous messaging (a message bus) is dropping a letter in the post: you publish an event and move on, and interested services pick it up on their own time — slower to "complete" but far more decoupled and resilient. A good rule: query synchronously, react asynchronously.
Synchronous with gRPC. gRPC sends compact binary over HTTP/2 and generates strongly-typed clients from a .proto contract, so calling another service feels like calling a local method — and it's several times faster than JSON-over-HTTP. Here's the real shape of a gRPC server and client.
Asynchronous with a message bus. Before the real broker, model the essential idea in plain C#: a publisher builds an immutable message record (the DTO — Data Transfer Object — both sides agree on) and a consumer handles it. Finish the two blanks below.
In a real system, MassTransit over RabbitMQ carries that record between two separately running services. The publisher fires an event and forgets it; any number of consumers subscribe without the publisher ever knowing they exist. That zero coupling is the superpower of async messaging.
3. Resilience — Expect Every Call to Fail
Over a network, calls will fail — timeouts, blips, a service restarting. A resilient system absorbs that instead of crashing. A retry tries a transient failure again, ideally with growing delays (exponential backoff) so you don't hammer a struggling service. A circuit breaker watches the failure rate and, once it's too high, "opens" to stop calls for a while — letting a sick service recover instead of drowning it. A timeout caps how long you'll wait. First, build the core retry loop yourself; finish the three blanks.
You'd never hand-write that in production — Polly (built into Microsoft.Extensions.Http.Resilience ) gives you retry, circuit breaker, and timeout as a configured pipeline on your HttpClient . Notice how the consuming service catches the final failure and returns a fallback — graceful degradation — rather than letting one dead dependency take down the whole request.
The single firmest rule in microservices: each service owns its data, and no other service touches it directly. The Orders service has the orders database; if Shipping needs order data, it asks Orders via an API or learns it from a published event — it never runs a query against the orders tables.
Why so strict? A shared database silently couples every service to one schema. Change a column and you risk breaking three teams' deployments at once — you've built a distributed monolith with all the cost of microservices and none of the independence.
The trade-off is that data is now spread out. When you need a consistent change across services, you use events and accept eventual consistency : Orders publishes OrderCreated , Inventory reacts a moment later. The system becomes correct soon , not instantly — and for most business workflows that's perfectly fine.
4. Observability — Seeing Inside a Distributed System
In a monolith you read one log file. With a request hopping across five services, you need observability : the three pillars are logs (structured, searchable records of events), metrics (numbers over time — request rate, error rate, latency), and traces (one request's whole journey across services, tied together by a shared correlation id ). .NET ships first-class support via ILogger for structured logging, OpenTelemetry for traces and metrics, and health checks so an orchestrator knows whether a service is alive.
5. Packaging with Docker
Each service ships as a container — your code plus exactly the runtime it needs, frozen into one image that runs identically on your laptop, in CI, and in production. That's what makes "deploy services independently" practical. A Dockerfile describes how to build the image; docker compose (or Kubernetes in production) runs the whole fleet together with its databases and message broker.
Reach for synchronous (gRPC/HTTP) when the caller genuinely needs the answer right now to continue — fetching a product's price to show on a page, validating a token. Keep these calls few and fast.
Reach for asynchronous (message bus) when something happened and others should react, but the caller doesn't need to wait — order placed, payment received, user signed up. This is the default for cross-service workflows because it decouples teams and survives a consumer being temporarily down.
The classic mistake is wiring everything synchronously: one slow service then stalls a whole chain of callers, and a single outage cascades. Events break that chain.
When you have a clear pressure they solve — independent scaling of one busy area, or multiple teams that need to deploy without blocking each other. If you don't have that yet, a modular monolith is simpler and faster to build. Microservices are an organisational and scaling tool, not a default.
Use gRPC for internal service-to-service calls where speed and a strongly-typed contract matter. Use REST/HTTP at the edge, for browsers and external clients that expect plain JSON. Many systems use both: gRPC inside, REST at the gateway.
Q: What is a circuit breaker and why not just retry forever?
Retrying a service that's genuinely down just adds load and delays the inevitable failure. A circuit breaker watches the failure rate and, once it's too high, stops calls for a cool-down window so the struggling service can recover — then cautiously tests again. It turns a slow cascade into a fast, contained failure.
Q: Can two services share one database to save effort?
No — that's the most common way to ruin a microservices design. A shared database couples both services to one schema and one deployment, recreating a monolith's rigidity with a network's fragility. Each service must own its data; share it through events and APIs.
Q: What does "eventual consistency" mean in practice?
Because each service has its own data, a change can't be instant everywhere. Orders publishes an event, Inventory reacts a moment later — so for a brief window the two are out of step, then they agree. For most business workflows that small delay is acceptable and far simpler than distributed transactions.
No blanks this time — just a brief and an outline. Build a minimal pub/sub MessageBus : one "service" subscribes a handler, another publishes a PaymentReceived event, and the bus delivers it. This is the real shape of a message broker, stripped to its essence. Run it and check your output against the expected line.
Practice quiz
Around what should each microservice's boundary be drawn?
- A database table
- A single class
- A bounded context — one self-contained slice of the business
- A UI page
Answer: A bounded context — one self-contained slice of the business. Each service owns one bounded context: a self-contained slice of the business with its own rules and data.
How do services share data correctly?
- Through contracts — messages or API responses, never the raw tables
- By querying each other's database tables
- Through a shared connection string
- They don't share at all
Answer: Through contracts — messages or API responses, never the raw tables. A service owns its data; others see a contract (message or API response), never the raw tables.
What is the firmest rule about data in microservices?
- One shared database for all services
- No databases allowed
- Databases must be in-memory
- Each service owns its own database; no other service touches it directly
Answer: Each service owns its own database; no other service touches it directly. Each service owns its data; a shared database recreates a monolith's coupling — a distributed monolith.
When is synchronous communication (gRPC/HTTP) the right choice?
- When something happened and others should react later
- When the caller genuinely needs the answer right now to continue
- Always
- Never
Answer: When the caller genuinely needs the answer right now to continue. Query synchronously when you need the reply now; react asynchronously to events.
What is the superpower of asynchronous messaging (a message bus)?
- Zero coupling — the publisher fires an event and any number of consumers react without it knowing
- It is always faster
- It needs no broker
- It guarantees exactly-once delivery
Answer: Zero coupling — the publisher fires an event and any number of consumers react without it knowing. The publisher fires and forgets; consumers subscribe without the publisher ever knowing they exist.
What does a circuit breaker do?
- Retries forever
- Speeds up calls
- Once the failure rate is too high, it stops calls for a while so a sick service can recover
- Encrypts traffic
Answer: Once the failure rate is too high, it stops calls for a while so a sick service can recover. A circuit breaker opens when failures are too high, stopping calls during a cool-down so the struggling service can recover.
What is a good resilience strategy for a transient failure?
- Crash immediately
- Retry, ideally with exponential backoff
- Ignore it
- Restart the whole system
Answer: Retry, ideally with exponential backoff. Retry transient failures with growing delays (exponential backoff) so you don't hammer a struggling service.
What does 'eventual consistency' mean in practice?
- Data is always instantly consistent everywhere
- Data is never consistent
- Only one service has data
- After an event, services agree soon rather than instantly
Answer: After an event, services agree soon rather than instantly. Because each service owns its data, a change propagates via events and the system becomes correct soon, not instantly.
Why should message consumers be idempotent?
- For speed
- Because a broker may deliver the same event more than once (at-least-once)
- To save memory
- It's required syntax
Answer: Because a broker may deliver the same event more than once (at-least-once). Brokers guarantee at-least-once delivery, so the same event can arrive twice; idempotent handlers produce the same result.
What is the 'distributed monolith' anti-pattern?
- A single deployable app
- A database per service
- Services so tightly chained by synchronous calls they must all deploy together
- An async event bus
Answer: Services so tightly chained by synchronous calls they must all deploy together. Tightly coupled services that must deploy together take on the network's pain without the independence — decouple with events and versioned contracts.