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.
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.
What You'll Learn
💡 Real-World Analogy
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.
📊 Monolith vs Microservices — the Trade-offs
Aspect
Monolith
Microservices
Deployment
One unit — simple to ship
Many units — deploy independently
Scaling
Scale the whole app together
Scale only the busy service
Data
One shared database
A database per service
Failure
One crash can stop everything
Failures isolated (if resilient)
Calls
In-process method calls (fast, reliable)
Network calls (slower, can fail)
Teams
Coordinate on one codebase
Each team owns a service
Complexity
Low to start; grows tangled
High up front (network, ops, tracing)
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.
🔎 Deep Dive: a Database per Service
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.
Sync or Async? A Quick Decision Guide
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.
Pro Tips
- 💡 Start with a modular monolith. Build clear bounded contexts in one deployable app first; extract a service only when a real scaling or team-ownership need appears. Premature microservices add cost with no benefit.
- 💡 Prefer events over chains of sync calls. If service A calls B calls C synchronously, one slow link stalls them all. Publish an event and let consumers react independently.
- 💡 Make every network call resilient. Wrap HttpClient with a Polly retry + circuit breaker + timeout pipeline by default — assume calls fail.
- 💡 One database per service, always. Sharing a database recreates a monolith's coupling with a network's fragility — the worst of both.
- 💡 Propagate a correlation id on every call so a single request can be traced end-to-end across all services in your logs.
- 💡 Design for idempotency. A message bus may deliver the same event twice — handlers should produce the same result if they run again.
Common Errors (and the fix)
- The distributed monolith: services that must all deploy together because they're chained by tight synchronous calls. You've taken on the network's pain without the independence. Fix: decouple with events and define stable, versioned contracts so services can change separately.
- Shared database between services: two services reading and writing the same tables couples them to one schema forever — change a column and unrelated services break. Fix: give each service its own database; share data via APIs and published events only.
- No resilience — cascading failure: one slow or down service makes every caller hang, and the failure spreads up the chain until the system falls over. Fix: add timeouts, retries with backoff, and a circuit breaker (Polly), plus a graceful fallback.
- Chatty synchronous calls: rendering one page fires twenty cross-service requests, so latency stacks up and reliability craters. Fix: batch requests, cache, or push the data ahead of time via events so the page needs no live calls.
- Ignoring duplicate messages: assuming an event arrives exactly once. Brokers guarantee at-least-once , so the same OrderCreated can arrive twice. Fix: make consumers idempotent (e.g. ignore an order id you've already processed).
📋 Quick Reference
Concern
Tool / Pattern
Use when
Fast sync call
gRPC
You need the reply now, low latency
Public sync call
HTTP / REST
Browsers or external clients
Async event
MassTransit + RabbitMQ
Something happened; others react
Resilience
Polly pipeline
Retry, circuit breaker, timeout
Data ownership
DB per service
Always — never share a database
Tracing
OpenTelemetry
Follow a request across services
Packaging
Docker + compose
Ship and run services consistently
Frequently Asked Questions
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.
Mini-Challenge: a Tiny In-Memory Message Bus
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.
🎉 Lesson Complete
- ✅ Draw service boundaries around bounded contexts — one service, one job, its own data
- ✅ Synchronous (gRPC/HTTP) for "I need the answer now"; asynchronous (message bus) for "this happened, react"
- ✅ Make every network call resilient — retry with backoff, circuit breaker, timeout, graceful fallback
- ✅ A database per service ; share data via events and APIs, embracing eventual consistency
- ✅ Observability — structured logs, metrics, distributed traces, and health checks
- ✅ Docker packages each service into a portable image you can deploy independently
- ✅ Avoid the distributed monolith , the shared database, and chatty sync chains
- ✅ Next lesson: Background Jobs — running reliable scheduled and queued work
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.
Continue this course
- Previous: Domain-Driven Design
- Next: Background Jobs