Your AI Agent Writes Code. But Does It Know Your Codebase? (Sponsored)
AI coding agents don't know your architecture, quality standards, or security policies. They generate code that works in isolation but violates your project boundaries β and you end up rewriting it. Sonar Context Augmentation uses MCP to dynamically serve each agent the exact context it needs for the coding task at hand, before it writes a single line β reducing rework, cutting token costs by up to 30%, and keeping AI-generated code production-ready from the first prompt.
Stop Switching Between Jira, Your IDE, and PRs β Let AI Connect the Dots (Sponsored)
Developers lose a lot of time jumping between tools just to understand what to build, write the code, and get it reviewed. Requirements live in Jira, context lives in Confluence, code lives in the IDE, and reviews happen in GitHub or Bitbucket. Rovo Dev from Atlassian brings all of that together with a context-aware AI agent that works across the entire development lifecycle.
Rovo Dev can generate code plans directly from Jira work items, implement changes β all with the full context of your requirements and existing codebase. It works inside VS Code and the CLI, so you stay in flow instead of tab-switching.
On the review side, Rovo Dev validates pull requests against the acceptance criteria defined in Jira, leaves inline PR comments with code suggestions, and supports customizable review rules.
Everything runs on Atlassian's enterprise-grade security model. It respects your existing permissions, doesn't use your data to train AI models, and meets SOC 1, SOC 2, and ISO 27001 compliance.
If your team already works in Jira and Bitbucket or GitHub, Rovo Dev fits right into that workflow β no new tools to adopt.
Choosing the right communication style between microservices is one of the most important architectural decisions you will make.
Get it wrong, and you end up with a fragile system where one failing service brings everything down.
Get it right, and your services stay independent, scalable, and resilient.
In distributed systems, there are two fundamental ways services talk to each other: synchronous and asynchronous communication.
Each comes with trade-offs in latency, coupling, reliability, and complexity.
Throughout my career, I have designed and built distributed systems using both approaches.
In this post, I will break down how each communication style works, when to use each one, and how real systems often combine both to achieve the best results.
In this post, we will explore:
How Microservices Communicate
Synchronous Communication: HTTP and gRPC
Asynchronous Communication: Message Brokers and Event Streaming
Synchronous vs Asynchronous: Deep Comparison
Hybrid Communication: Mixing Sync and Async in One Flow
In a Modular Monolith application, components call each other through in-process method calls.
It's fast, simple, and reliable.
But when you break a Modular Monolith into microservices, those calls become network calls.
And network calls introduce latency, failures, and a whole new set of challenges.
There are two fundamental approaches for microservices communication:
Synchronous communication: the caller sends a request and waits for a response before continuing.
The caller is blocked until the response arrives or a timeout occurs.
Think of it like a phone call - you dial, the other person picks up, you talk, and you wait for an answer before continuing the conversation.
Asynchronous communication: the caller sends a message and moves on without waiting for an immediate response.
The message is processed later by the receiving service at its own pace.
Think of it like sending an email - you send it and continue with your work, without waiting for the recipient to read and reply.
These two styles lead to fundamentally different system behaviors in terms of coupling, resilience, and scalability.
Now let's explore each communication style in detail.
Synchronous communication is the most intuitive way for services to talk.
Service A calls Service B, waits for the result, and then continues its work.
The two most popular protocols for synchronous communication in .NET are HTTP (REST) and gRPC.
You can also use HotChocolate GraphQL Strawberry Shake for HTTP/REST communication. But this is a pretty niche approach. If you want to learn more about HotChocolate GraphQL, see this article.
HTTP-based REST APIs are the most common way microservices communicate synchronously.
They are simple to build, easy to debug, and supported by virtually every language and framework.
Here is a typical example. The Booking Service needs to check room availability from the Property Service before confirming a reservation:
You want broad compatibility across languages and platforms
The main downside is temporal coupling - the caller must wait for the receiver to respond.
If the Property Service is slow or down, the Booking Service is stuck.
gRPC is a high-performance RPC (Remote Procedure Call) framework built on HTTP/2.
It uses Protocol Buffers (Protobuf) for serialization, which is significantly faster and smaller than JSON.
gRPC shines in service-to-service communication where performance matters.
Let's explore an example: how to check the room availability with gRPC.
First, define the service contract in a .proto file:
You need low-latency, high-throughput communication between internal services
You have strict performance requirements
You want strongly typed contracts between services
You need streaming capabilities (server streaming, client streaming, or bidirectional)
gRPC is not ideal for browser-to-service communication (though gRPC-Web exists as a workaround) or when you need human-readable payloads for debugging.
The biggest risk with synchronous communication is cascading failures.
When Service A calls Service B, and Service B calls Service C - if Service C is down, all three services are affected.
The AddStandardResilienceHandler from Microsoft.Extensions.Http.Resilience gives you retries, circuit breaker, and timeouts in a single configuration.
Circuit Breaker - stops calling a failing service to give it time to recover.
When a service fails repeatedly, the circuit "opens," and all subsequent requests fail immediately instead of waiting for a timeout.
After a cooldown period, the circuit "half-opens" to test if the service has recovered.
These patterns don't eliminate the problem of synchronous coupling.
They reduce the impact.
If you need true independence between services, you need asynchronous communication.
Asynchronous Communication: Message Brokers and Event Streaming
Asynchronous communication decouples services.
The sender publishes a message and moves on.
The receiver processes it when ready.
This decoupling makes your system more resilient: if a downstream service is temporarily unavailable, messages wait in the queue until the service comes back online.
There are several communication patterns that fall under the asynchronous style.
Point-to-Point integration: when one service communicates directly with another service by sending a message to a specific queue.
In Point-to-Point, there is only one receiver.
Publish-Subscribe: when a service publishes an event, and multiple subscribers can react to it independently.
When a consumer fails to process a message, you don't want it to be lost.
Dead-letter queues capture messages that couldn't be processed after a certain number of retries.
This lets you inspect failed messages, fix the issue, and reprocess them later.
Azure Service Bus supports dead-letter queues out of the box.
It's a common misconception that message queues only support fire-and-forget.
You can also implement a Request-Response pattern over message queues.
In this pattern, the sender publishes a request message with a correlation ID and a reply-to queue.
The receiver processes the request and sends a response back to the reply-to queue.
The sender correlates the response using the correlation ID.
This gives you the decoupling benefits of async communication while still getting a response.
And both services in Request-Response async communication don't know about each other.
You may consider Request-Response over queues when:
You need a response but don't want tight synchronous coupling
You want the broker to act as a buffer between services
It adds complexity compared to simple fire-and-forget, so use it only when you genuinely need the response back.
And for such request-response cases, gRPC or REST can be a better choice.
Event streaming follows a Publish-Subscribe model.
A producer publishes events to a topic, and multiple consumers can independently subscribe and process those events.
Unlike message queues, events are not removed after consumption.
They are stored for a configurable retention period, which allows consumers to replay events if needed.
This is ideal for broadcasting facts - "a booking was created", "a payment was processed", "a guest checked in".
Multiple services can react to the same event independently.
Popular event streaming technologies include:
Apache Kafka - the industry standard for high-throughput event streaming
Azure Event Hubs - a fully managed event streaming platform on Azure
Here is an example of publishing a booking event to Kafka:
And consuming events in multiple independent services:
csharp
1publicclassBookingEventConsumer:BackgroundService2{3privatereadonlyIConsumer<string,string> _consumer;4privatereadonlyILogger<BookingEventConsumer> _logger;56publicBookingEventConsumer(7IConfiguration configuration,8ILogger<BookingEventConsumer> logger)9{10 _logger = logger;1112var config =newConsumerConfig13{14 BootstrapServers = configuration["Kafka:BootstrapServers"],15 GroupId ="analytics-service",16 AutoOffsetReset = AutoOffsetReset.Earliest,17 EnableAutoCommit =false18};1920 _consumer =newConsumerBuilder<string,string>(config).Build();21}2223protectedoverrideasyncTaskExecuteAsync(CancellationToken stoppingToken)24{25 _consumer.Subscribe("booking-events");2627await Task.Run(()=>28{29while(!stoppingToken.IsCancellationRequested)30{31var result = _consumer.Consume(stoppingToken);3233var bookingEvent = JsonSerializer
34.Deserialize<BookingCreated>(result.Message.Value);3536 _logger.LogInformation(37"Processing booking event {BookingId} for analytics",38 bookingEvent!.BookingId);3940// Process the event for analytics4142 _consumer.Commit(result);43}44}, stoppingToken);45}46}
The key difference from message queues: multiple consumer groups can independently read the same events.
The Analytics Service, Notification Service, and Reporting Service can all subscribe to booking-events and process them independently.
If you want to simplify working with message brokers in .NET, consider MassTransit or Wolverine.
MassTransit is an open-source abstraction layer that works with RabbitMQ, Azure Service Bus, Amazon SQS, and Kafka.
It provides a consistent API, automatic retries, dead-letter handling, saga orchestration, and much more.
However, MassTransit recently moved to a commercial license model for production use.
But still, MassTransit V8 will remain free with support until the end of 2026.
If you're looking for an alternative, consider Wolverine.
Hybrid Communication: Mixing Sync and Async in One Flow
In practice, most microservice architectures don't use purely synchronous or purely asynchronous communication.
They mix both styles in the same request flow.
The idea is simple: use synchronous communication where you need an immediate response, and asynchronous communication for everything that can happen in the background.
Here is a common pattern. A user books a hotel room:
The API Gateway receives the booking request (sync HTTP)
The Booking Service calls the Property Service to verify availability (sync gRPC)
The Booking Service calls the Payment Service to charge the guest (sync HTTP)
The Booking Service confirms the booking and publishes a BookingConfirmed event (async)
The Notification Service picks up the event and sends a confirmation email (async)
The Analytics Service picks up the same event and updates dashboards (async)
The Property Service picks up the same event and updates availability (async)
Steps 1-3 are synchronous because the user is waiting for a response.
Steps 5-7 are asynchronous because they don't affect the user's immediate experience.
In some workflows, you can move Payment to an async flow as well, depending on your requirements.
For example, after booking, the user is redirected to a payment page and may receive an email notification that their booking is being processed and is awaiting payment.
While the payment happens in the background, the user sees it as a seamless process in the same browser tab.
On the backend, it can be an asynchronous flow.
Once the payment is confirmed, you send an email notification that your booking is now active.
This hybrid approach gives you the best of both worlds:
The user gets an immediate confirmation because the critical steps (availability check, payment) happen synchronously
Background work (notifications, analytics, availability updates) happens asynchronously without blocking the user
If the Notification Service is temporarily down, the confirmation email is not lost - it waits in the message queue
The key principle is: be synchronous at the boundary, be asynchronous behind it.
What happens at the boundary (user-facing API) should be fast and responsive.
What happens behind the boundary (service-to-service coordination) should be resilient and decoupled.
Synchronous and asynchronous communication are both essential tools in a microservice architecture.
Neither one is universally better - they solve different problems.
Synchronous communication (HTTP, gRPC) is simple, immediate, and easy to reason about.
It is the right choice when users need real-time responses and strong consistency.
Asynchronous communication (RabbitMQ, Kafka, Azure Service Bus, Azure Event Hubs) is resilient, scalable, and decoupled.
It is the right choice for background processing, event-driven workflows, and broadcasting events to multiple services.
Most production systems use a hybrid approach - synchronous at the user-facing boundary and asynchronous behind it.
The key decisions come down to:
Does the caller need a response right now?
Can this work happen in the background?
How many services need to react?
Start simple. Use synchronous communication where it's natural.
Introduce asynchronous communication where you need resilience and scalability.
And always design for failure - regardless of which style you choose.
Hope you find this newsletter useful. See you next time.
Whenever you're ready, here's how I can help you:
The .NET Senior Playbook is built to:
Fast-track you from junior or mid-level to senior
Keep you growing as a senior
Help you beat any .NET interview
Covers everything: C#, ASP.NET Core, EF Core, system design β answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.
Not sure where you stand? Take the free .NET Interview Run:
Find out your real level β Junior to Senior+
A realistic mock .NET interview β across 13 areas of C#, .NET, ASP.NET Core and System Design
No credit card required. When you finish, you get a personalized report: your level, your strongest and weakest areas, and where to focus next β the perfect way to benchmark yourself before diving into the Playbook.