Microservices in .NET
Reviewed & published by Brayan K
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 worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Draw service boundaries around bounded contexts (one service, one job)
- Choose synchronous gRPC/HTTP vs asynchronous message-bus communication
- Make calls resilient with retries, circuit breakers, and timeouts (Polly)
- Give each service its own database — and why a shared one is an anti-pattern
- Add observability: structured logs, traces, and health checks
- Package and run services with Docker for consistent deployment
💡 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.
using System;
// Each microservice owns ONE bounded context — a slice of the business
// with its OWN data and its OWN rules. Services never reach into each
// other's tables; they talk only through contracts (messages or APIs).
class Program
{
static void Main()
{
// Three independent services, each modelled here as a tiny class.
var orders = new OrderService();
var inventory = new InventoryService();
var shipping = new ShippingService();
// The Order service owns the order. It hands the OTHER services a
// SUMMARY (a contract) — not direct access to its database.
var summary = orders.PlaceOrder("SKU-42", quantity: 2);
inventory.Reserve(summary.Sku, summary.Quantity);
shipping.Schedule(summary.OrderId);
}
}
class OrderService
{
// This data belongs to OrderService alone.
private int _nextId = 1000;
public OrderSummary PlaceOrder(string sku, int quantity)
{
int id = _nextId++;
Console.WriteLine($"[orders] Order {id} placed: {quantity} x {sku}");
return new OrderSummary(id, sku, quantity); // share a contract, not the DB
}
}
class InventoryService
{
public void Reserve(string sku, int quantity) =>
Console.WriteLine($"[inventory] Reserved {quantity} x {sku}");
}
class ShippingService
{
public void Schedule(int orderId) =>
Console.WriteLine($"[shipping] Scheduled delivery for order {orderId}");
}
// A small immutable contract passed BETWEEN services (no shared database).
record OrderSummary(int OrderId, string Sku, int Quantity);
// ✅ Expected output:
// [orders] Order 1000 placed: 2 x SKU-42
// [inventory] Reserved 2 x SKU-42
// [shipping] Scheduled delivery for order 10002. 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.
// ══════════════════════════════════════════════
// gRPC — fast, strongly-typed SYNC calls between services
// (real .NET code; needs Grpc.AspNetCore + a .proto file to run)
// ══════════════════════════════════════════════
// product.proto — the contract both client and server are generated from:
// syntax = "proto3";
// service ProductService {
// rpc GetProduct (ProductRequest) returns (ProductReply);
// }
// message ProductRequest { int32 id = 1; }
// message ProductReply { int32 id = 1; string name = 2; double price = 3; }
using Grpc.Core;
// SERVER — implement the generated base class.
public class ProductGrpcService : ProductService.ProductServiceBase
{
private readonly IProductRepository _repo;
public ProductGrpcService(IProductRepository repo) => _repo = repo;
public override async Task<ProductReply> GetProduct(
ProductRequest request, ServerCallContext context)
{
var product = await _repo.GetByIdAsync(request.Id);
if (product is null)
// gRPC has typed status codes — like HTTP statuses, but built in.
throw new RpcException(new Status(StatusCode.NotFound, "Not found"));
return new ProductReply
{
Id = product.Id,
Name = product.Name,
Price = (double)product.Price
};
}
}
// CLIENT — another service calls it like a local method, fully typed.
public class Catalog
{
private readonly ProductService.ProductServiceClient _client;
public Catalog(ProductService.ProductServiceClient client) => _client = client;
public async Task<string> GetNameAsync(int id)
{
ProductReply reply = await _client.GetProductAsync(
new ProductRequest { Id = id });
return reply.Name;
}
}
// ✅ At runtime: a GetProductAsync(42) call returns a strongly-typed
// ProductReply in a few milliseconds — no JSON parsing, no string URLs.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.
using System;
// 🎯 YOUR TURN — model async messaging with a plain DTO (no broker needed).
// A 'publisher' builds a message record; a 'consumer' handles it. In real
// systems a message bus carries this record between two separate services.
// A message DTO is an IMMUTABLE record — the contract both sides agree on.
record OrderCreated(int OrderId, string CustomerEmail, decimal Total);
class Program
{
static void Main()
{
// 1) PUBLISH: build the message the Order service would send.
var message = new OrderCreated(
OrderId: 7,
CustomerEmail: "[email protected]",
Total: 49.99m); // 👉 already filled — this is the contract
// 2) CONSUME: hand the message to the handler (the Email service).
Handle(___); // 👉 pass the message variable: message
}
// The consumer reacts to the event — here it 'sends' a confirmation email.
static void Handle(OrderCreated msg)
{
// 3) Read fields off the record to build the email body.
Console.WriteLine($"Emailing {msg.CustomerEmail}");
Console.WriteLine($"Order {msg.OrderId} confirmed — total £{msg.Total}");
}
}
// ✅ Expected output:
// Emailing [email protected]
// Order 7 confirmed — total £49.99In 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.
// ══════════════════════════════════════════════
// MassTransit + RabbitMQ — ASYNC, decoupled messaging
// (real .NET code; needs MassTransit + a running broker to execute)
// ══════════════════════════════════════════════
using MassTransit;
// Shared contract — lives in a package both services reference.
public record OrderCreated(Guid OrderId, string Email, decimal Total);
// PUBLISHER — the Order service fires an event and forgets about it.
// It does NOT know or care who is listening. Zero coupling.
public class OrderService
{
private readonly IPublishEndpoint _bus;
public OrderService(IPublishEndpoint bus) => _bus = bus;
public async Task CreateAsync(string email, decimal total)
{
var id = Guid.NewGuid();
// ...save the order to OUR database...
await _bus.Publish(new OrderCreated(id, email, total)); // fire & forget
}
}
// CONSUMER — the Email service subscribes and reacts on its own time.
// Add more consumers (Inventory, Analytics) without touching the publisher.
public class EmailConsumer : IConsumer<OrderCreated>
{
public Task Consume(ConsumeContext<OrderCreated> ctx)
{
var msg = ctx.Message;
Console.WriteLine($"Emailing {msg.Email} about order {msg.OrderId}");
return Task.CompletedTask;
}
}
// Program.cs — wire MassTransit to RabbitMQ.
builder.Services.AddMassTransit(x =>
{
x.AddConsumer<EmailConsumer>();
x.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host("rabbitmq://localhost");
cfg.ConfigureEndpoints(ctx); // auto-creates queues for each consumer
});
});
// ✅ At runtime: CreateAsync(...) publishes once; the broker delivers a copy
// to EVERY subscribed consumer. The publisher never blocks waiting for them.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.
using System;
// 🎯 YOUR TURN — finish the retry loop, then run it.
// A remote service is flaky: it fails the first 2 attempts, then succeeds.
// Real resilience (Polly) does exactly this — retry up to N times, then give up.
class Program
{
static int _attempts = 0;
static void Main()
{
int maxRetries = 3; // try at most 3 times before giving up
bool success = false;
// 1) Loop from attempt 1 up to and including maxRetries.
for (int attempt = 1; attempt <= ___; attempt++) // 👉 the limit: maxRetries
{
Console.WriteLine($"Attempt {attempt}...");
if (TryCallFlakyService())
{
// 2) The call worked — flip success to true and stop retrying.
success = ___; // 👉 the value: true
Console.WriteLine("Success!");
break; // leave the loop early
}
Console.WriteLine(" failed, will retry");
}
// 3) If every attempt failed, degrade gracefully instead of crashing.
if (!success)
Console.WriteLine("Gave up after all retries.");
}
// Returns false twice (simulated failure), then true on the 3rd call.
static bool TryCallFlakyService()
{
_attempts++;
return _attempts >= 3;
}
}
// ✅ Expected output:
// Attempt 1...
// failed, will retry
// Attempt 2...
// failed, will retry
// Attempt 3...
// Success!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.
// ══════════════════════════════════════════════
// Polly via Microsoft.Extensions.Http.Resilience
// retry + circuit breaker + timeout on an HttpClient
// (real .NET code; needs the resilience NuGet package to run)
// ══════════════════════════════════════════════
using Polly;
// Program.cs — every call through this client is now resilient.
builder.Services.AddHttpClient("products", c =>
c.BaseAddress = new Uri("https://product-service:5001"))
.AddResilienceHandler("pipeline", b =>
{
// RETRY — try again on transient failures, backing off each time.
b.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential, // 1s, 2s, 4s...
});
// CIRCUIT BREAKER — if the service keeps failing, STOP calling it for a
// while so you don't pile on load and cascade the failure everywhere.
b.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // open at 50% failures
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(15), // stay open 15s, then test again
});
// TIMEOUT — never wait forever for one slow call.
b.AddTimeout(TimeSpan.FromSeconds(10));
});
// Consuming service — degrade gracefully when the pipeline finally gives up.
public class Catalog
{
private readonly HttpClient _http;
public Catalog(IHttpClientFactory f) => _http = f.CreateClient("products");
public async Task<string> GetNameAsync(int id)
{
try
{
var p = await _http.GetFromJsonAsync<Product>($"/api/products/{id}");
return p?.Name ?? "unknown";
}
catch (HttpRequestException)
{
// All retries failed OR the circuit is open — return a fallback
// instead of crashing the whole request.
return "temporarily unavailable";
}
}
}
// ✅ At runtime: a brief blip is retried and recovers silently; a sustained
// outage trips the breaker so callers fail fast with the fallback value.🔎 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.
orders-service ──▶ orders-db (only orders-service connects)
inventory-service ──▶ inventory-db (only inventory-service connects)
shipping-service ──▶ shipping-db (only shipping-service connects)
// They share DATA via events/APIs — never a shared connection string.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.
// Structured logging — log VALUES, not pre-built strings, so they're queryable.
_logger.LogInformation("Order {OrderId} created for {Email}", id, email);
// OpenTelemetry — automatic distributed traces across HTTP and gRPC calls.
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation());
// Health checks — the orchestrator polls /health to decide if you're ready.
builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");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.
# Dockerfile — multi-stage: build with the SDK, run on the slim runtime.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish OrderService -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:8.0 # smaller runtime-only image
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "OrderService.dll"]
# docker-compose.yml — run the services + their infrastructure together.
# services:
# orders: { build: ./OrderService, ports: ["5001:8080"] }
# inventory: { build: ./InventoryService, ports: ["5002:8080"] }
# rabbitmq: { image: rabbitmq:3-management }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
Q: When should I actually use microservices?
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.
Q: gRPC or REST between services?
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.
using System;
using System.Collections.Generic;
// 🎯 MINI-CHALLENGE: a tiny in-memory message bus
// Model pub/sub with no broker — one service publishes, another handles.
//
// 1. Define a record PaymentReceived(int OrderId, decimal Amount).
// 2. In MessageBus:
// - Keep a List of handlers: List<Action<PaymentReceived>>
// - Subscribe(handler): add the handler to the list.
// - Publish(message): call EVERY subscribed handler with the message.
// 3. In Main:
// - Create a bus.
// - Subscribe a handler that prints: Receipt for order {OrderId}: £{Amount}
// - Publish a new PaymentReceived(5, 19.99m).
//
// ✅ Expected output:
// Receipt for order 5: £19.99
record PaymentReceived(int OrderId, decimal Amount);
class MessageBus
{
// your handler list, Subscribe and Publish go here
}
class Program
{
static void Main()
{
// your code here
}
}🎉 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
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 (Aggregates, Value Objects, Repositories)
- Next: Background Jobs with Hangfire & Quartz.NET — Schedule and run background tasks reliably with Hangfire and Quartz.NET
- Quick reference: C# cheat sheet