Dapr for Beginners: Simplifying Microservices with Building Blocks
-
Ahmed Muhi - 16 Apr, 2023
Introduction: The Tight Coupling Problem
Splitting a monolith into services does not automatically make the system loosely coupled.
You may have separate services, separate codebases, and separate teams, but the workflow can still be tightly connected.
Imagine an e-commerce checkout.
A customer places an order, and the Order Service needs several things to happen:
Reduce inventory
Create a shipment
Send a confirmation email
Record analytics
Process payment
A simple design is to make the Order Service call each downstream service directly.
Order Service
→ Inventory Service
→ Shipping Service
→ Notification Service
→ Analytics Service
→ Payment Service
At first, this feels reasonable.
The flow is easy to understand. The Order Service controls the process. Each service returns a response, and the Order Service moves to the next step.
But this design creates a problem.
The Order Service is now coupled to every service it calls.
If the Shipping Service becomes slow, checkout becomes slow. Even if inventory and payment are working fine, the customer is still waiting because the Order Service is waiting.
If the Notification Service is unavailable, the order flow may fail or become harder to reason about.
If the Analytics Service changes its API response, the Order Service may need to change as well.
And as more services are added, the Order Service becomes less like a clean business service and more like a coordinator for everyone else’s APIs.
The services are separate, but the workflow is still tightly coupled.
That coupling shows up in three ways:
Runtime coupling
→ one slow or failing service can affect the whole flow
Deployment coupling
→ one service change can force another service to change
Knowledge coupling
→ the Order Service needs to know who must be called and how
This is one of the first problems teams hit when they move from a monolith to microservices.
The architecture has more services, but those services still depend on each other too directly.
The Publisher/Subscriber pattern exists to reduce that coupling.
Instead of the Order Service calling every interested service directly, it can publish an event that says:
OrderPlaced
Then other services can react to that event independently.
That is the shift this article is about:
Direct service calls
→ services know and wait on each other
Publisher/Subscriber
→ services communicate through events
Pub/Sub is not magic, and it is not the right pattern for every workflow.
But when the problem is tight coupling between services, it gives you a powerful way to let services react without forcing the publisher to know every subscriber.
What Pub/Sub Changes
Pub/Sub changes the direction of responsibility.
With direct service calls, the Order Service needs to know who should be called:
Inventory Service
Shipping Service
Notification Service
Analytics Service
Payment Service
It also needs to know when to call them, how to call them, and what to do if one of them is slow or unavailable.
With Pub/Sub, the Order Service does something simpler.
When an order is placed, it publishes an event:
OrderPlaced
That event says:
Something happened.
An order was placed.
Here is the information about it.
The Order Service publishes the event to a message broker and moves on.
It does not need to know whether the Shipping Service exists.
It does not need to know whether the Notification Service has changed.
It does not need to know whether the Analytics Service was added yesterday.
Those services subscribe to the events they care about.
So the model changes from this:
Order Service
→ calls Inventory Service
→ calls Shipping Service
→ calls Notification Service
→ calls Analytics Service
to this:
Order Service
→ publishes OrderPlaced event
→ message broker
→ subscribers react independently
That is the core shift.
The Order Service no longer coordinates every downstream action. It announces that an important business event happened, and each subscriber decides what to do with that event.
For example:
Inventory Service
→ subscribes to OrderPlaced
→ reduces stock
Shipping Service
→ subscribes to OrderPlaced
→ creates a shipment
Notification Service
→ subscribes to OrderPlaced
→ sends a confirmation email
Analytics Service
→ subscribes to OrderPlaced
→ records metrics
This gives you three practical benefits.
First, it reduces runtime coupling.
If the Analytics Service is slow, the Order Service does not need to wait for it before continuing. The event can be processed independently.
Second, it reduces deployment coupling.
If the Notification Service changes internally, the Order Service does not need to change as long as the event contract remains stable.
Third, it reduces knowledge coupling.
The Order Service does not need to know every service that wants to react to an order. New subscribers can be added later without changing the publisher.
That is the power of Pub/Sub.
It does not remove all complexity. It moves the system from direct coordination to event-based coordination.
That trade-off matters, and we will come back to it later.
But first, let’s name the moving parts that make the pattern work.
The Four Parts of Pub/Sub
Every Pub/Sub system has the same basic shape.
There is something that publishes an event, something that receives the event, and something in the middle that routes the event to the right places.
The pattern looks like this:

There are four core parts.
Publisher
The publisher is the service that announces that something happened.
In our example, the Order Service is the publisher.
When an order is placed, it publishes an event:
OrderPlaced
The publisher does not need to know every service that will react to that event.
Its job is to publish the event with the information subscribers need.
Message Broker
The message broker is the middle layer.
It receives events from publishers and delivers them to interested subscribers.
This is what breaks the direct connection between services.
The Order Service does not call Shipping, Notifications, or Analytics directly. It sends the event to the broker. The broker handles delivery to the subscribers.
Topic
A topic is a named channel for a type of event.
For example:
OrderPlaced
PaymentProcessed
InventoryUpdated
ShipmentCreated
Publishers send events to a topic.
Subscribers subscribe to topics they care about.
The topic gives the system a way to route events without the publisher knowing every consumer.
Subscriber
The subscriber is the service that reacts to an event.
For example:
Inventory Service
→ reacts to OrderPlaced by reducing stock
Shipping Service
→ reacts to OrderPlaced by creating a shipment
Notification Service
→ reacts to OrderPlaced by sending an email
Analytics Service
→ reacts to OrderPlaced by recording metrics
Each subscriber owns its own reaction.
That is important.
The publisher announces the event, but it does not control how every subscriber responds.
So the full model is:
Publisher
→ publishes event to a topic
Message broker
→ receives and routes the event
Subscribers
→ receive the event and react independently
That is the basic Pub/Sub pattern.
Now we can look at how Azure implements this idea with different services.
Pub/Sub in Azure: Service Bus, Event Grid, and Event Hubs
Azure has more than one service that can support Pub/Sub-style architectures.
That can be confusing at first.
The question is not:
Which Azure messaging service is the best?
The better question is:
What kind of event am I publishing,
and what does the subscriber need from the platform?
Azure Service Bus, Event Grid, and Event Hubs can all help decouple publishers from consumers, but they are designed for different types of workloads.
Azure Service Bus: business messages that must be processed
Azure Service Bus is usually the best fit when you are dealing with business-critical messages.
Think about events like:
OrderPlaced
PaymentProcessed
InvoiceCreated
InventoryReserved
These are not casual notifications. They represent business activity that must be processed reliably.
If a subscriber is offline, Service Bus can hold the message until the subscriber is ready. If a message cannot be processed successfully, it can be moved to a dead-letter queue for investigation. If related messages need to be processed in order, Service Bus has features that can help with that.
Use Service Bus when:
Every message matters.
Subscribers may be temporarily offline.
You need queues, topics, subscriptions, and dead-letter handling.
The message represents business workflow or business state.
For the e-commerce example, Service Bus is often the natural choice for OrderPlaced because losing an order-related event is not acceptable.
Azure Event Grid: reactive events and Azure resource notifications
Azure Event Grid is built for event-driven reactions.
It is especially useful when something happens and another service needs to react quickly.
Examples include:
A file is uploaded to Blob Storage.
A resource is created in Azure.
An image needs thumbnail processing.
A serverless function should run when an event happens.
Event Grid is also deeply integrated with Azure services. Many Azure resources can publish events to Event Grid without you writing custom publisher logic.
Use Event Grid when:
You want services to react to events quickly.
The event is a notification that something happened.
You are integrating with Azure resource events.
You are building serverless event handlers with Azure Functions.
Event Grid is a good fit for reactive workflows, but it is not usually the first choice when you need queue-like business message processing with long-lived backlogs and dead-letter investigation as the main model.
Azure Event Hubs: high-volume event streams
Azure Event Hubs is designed for streaming.
It is built for very high volumes of events, such as:
IoT sensor readings
Application logs
Telemetry streams
Clickstream data
Real-time analytics feeds
The shape is different from typical business messaging.
With Event Hubs, you are often dealing with a continuous stream of events rather than a small number of discrete business messages.
Use Event Hubs when:
You need to ingest a very large volume of events.
Consumers process events as a stream.
You care about throughput and stream processing.
You may want to store the stream for later analytics.
Event Hubs is the right tool when the problem looks like telemetry, streaming, or analytics ingestion.
It is usually not the first place I would start for ordinary business events like a small set of order-processing messages.
Choosing between them
A simple way to choose is this:
Use Service Bus
→ for business messages that must be processed reliably.
Use Event Grid
→ for reactive event notifications, especially from Azure resources.
Use Event Hubs
→ for high-volume event streams and telemetry.
Or, framed as questions:
Is every message business-critical and must be processed?
→ Start with Service Bus.
Is this mainly a notification that something happened?
→ Look at Event Grid.
Is this a massive stream of events or telemetry?
→ Look at Event Hubs.
All three services can participate in event-driven designs.
The important part is choosing the service that matches the shape of the problem.
Operational Realities
Pub/Sub reduces coupling, but it does not remove responsibility.
When services communicate through events, the system becomes more flexible, but it also becomes more asynchronous. That means you need to design for a few realities that direct service calls can sometimes hide.
Subscribers must be idempotent
In a Pub/Sub system, a subscriber may receive the same event more than once.
That can happen because of retries, network issues, restarts, acknowledgements that fail, or broker delivery behaviour.
So subscribers should be idempotent.
That means processing the same event more than once should not create the wrong result.
For example, imagine a payment subscriber receives this event:
{
"eventId": "evt-123",
"eventType": "OrderPlaced",
"orderId": "ord-456",
"amount": 99.99
}
If the subscriber charges the customer and then fails before acknowledging the message, the broker may deliver the same event again.
Without idempotency, the customer could be charged twice.
With idempotency, the subscriber checks whether it has already processed evt-123 before doing the work again.
The basic idea is:
If this event was already processed
→ acknowledge it and skip the work
If this event is new
→ process it
→ record that it was processed
→ acknowledge it
Pub/Sub systems need this kind of defensive design.
Events should include enough context
Another common mistake is publishing events that are too thin.
For example:
{
"eventType": "OrderPlaced",
"orderId": "ord-456"
}
That looks clean, but it may force every subscriber to call the Order Service to get the details it needs.
The Shipping Service needs the delivery address.
The Notification Service needs the customer email.
The Analytics Service may need the order total, currency, and item list.
If every subscriber has to call back to the Order Service, you have reintroduced coupling through the back door.
A better event includes the context subscribers need to do their work:
{
"eventId": "evt-123",
"eventType": "OrderPlaced",
"orderId": "ord-456",
"customerId": "cust-789",
"customerEmail": "customer@example.com",
"items": [
{
"sku": "item1",
"quantity": 2
}
],
"totalAmount": 99.99,
"currency": "AUD",
"createdAt": "2026-05-02T10:00:00Z"
}
The goal is not to put the entire database record into every event.
The goal is to include enough information for subscribers to process the event without immediately depending on the publisher being available.
Failed messages need a recovery path
Sooner or later, a subscriber will fail to process an event.
The event might be malformed. The subscriber might have a bug. A downstream database might be unavailable. A required field might be missing.
The question is not whether failures happen.
The question is what happens when they do.
A good Pub/Sub design needs a recovery path:
Retry temporary failures
Move poison messages aside
Alert someone when messages cannot be processed
Allow failed messages to be inspected and replayed if appropriate
This is where dead-letter queues matter.
A dead-letter queue gives failed messages somewhere to go after they cannot be processed successfully. That prevents one bad message from blocking the rest of the flow.
For example, if a PaymentProcessed event is missing a required field, the accounting subscriber should not retry it forever and block every valid payment message behind it.
After a configured number of failures, the message should move to a dead-letter queue for investigation.
The system becomes eventually consistent
With direct calls, the caller often waits until the work is finished.
With Pub/Sub, the publisher usually publishes the event and moves on.
That means subscribers may process the event seconds later, minutes later, or after recovering from an outage.
So the system becomes eventually consistent.
The order may be placed now, but the confirmation email, shipping record, analytics update, or inventory projection may catch up shortly after.
That is not automatically bad.
It is often exactly what makes Pub/Sub useful.
But it needs to match the business process. Some workflows can tolerate delay. Others need an immediate answer.
That is the trade-off.
Pub/Sub gives you decoupling, fanout, and failure isolation, but it also asks you to design for duplicate delivery, event contracts, failed messages, and eventual consistency.
When Pub/Sub Is Not the Right Pattern
Pub/Sub is powerful, but it is not the answer to every communication problem.
The key question is:
Does the publisher need an immediate answer?
If the answer is yes, Pub/Sub may be the wrong shape.
When the caller needs an immediate decision
Some workflows need request/response communication.
For example, during checkout, the Order Service may need to know:
Did the payment succeed?
Is the item actually available?
Can this order be accepted?
Those are decisions the caller needs before it can continue.
In that case, publishing an event and hoping another service reacts later is not enough. The Order Service needs a response now.
That does not mean Pub/Sub has no role in checkout.
It means Pub/Sub should not replace every direct call.
A common design is:
Use request/response
→ for decisions that must happen now
Use Pub/Sub
→ for reactions that can happen after the main decision
For example:
Payment authorization
→ request/response
Send confirmation email
→ Pub/Sub
Update analytics
→ Pub/Sub
Start fulfilment workflow
→ Pub/Sub
When strict global ordering is required
Pub/Sub can preserve ordering in some scopes, depending on the broker and configuration.
But if your workflow needs every subscriber to process every event in one strict global order, Pub/Sub can become difficult.
Financial ledger systems are a good example. If every transaction must be processed in exact order across the whole system, you need to be very careful about partitioning, ordering guarantees, replay, and consistency.
In those cases, you may need a different pattern or a more specialised event design.
Pub/Sub can still be part of the architecture, but you should not assume ordering is automatic just because messages are flowing through a broker.
When the system is still small and simple
Pub/Sub adds moving parts.
You need a broker. You need topics or subscriptions. You need monitoring. You need idempotent subscribers. You need failure handling. You need to reason about eventual consistency.
If you only have a few services and a small team, direct calls may be easier to understand and operate.
That is not a failure.
It is good design discipline.
Start with the simplest communication pattern that works. Add Pub/Sub when you are solving a real coupling problem, such as:
One service is calling too many downstream services.
A slow subscriber is blocking the main workflow.
New consumers need to react without changing the publisher.
Teams need to deploy independently.
Failures should be isolated behind a broker.
The point is not to use Pub/Sub because it sounds cloud-native.
The point is to use it when asynchronous event-based communication matches the problem.
The Pub/Sub Mental Model
Pub/Sub is a way to let services communicate without forcing them to know too much about each other.
The publisher announces that something happened.
The broker receives that event and routes it to interested subscribers.
Each subscriber reacts in its own way.
The simple model is:
Publisher
→ publishes an event
Broker
→ routes the event
Subscribers
→ react independently
The most important part is the direction of knowledge.
In direct service calls, the caller usually knows the services it needs to call:
Order Service
→ call Inventory
→ call Shipping
→ call Notifications
→ call Analytics
In Pub/Sub, the publisher knows the event, not every consumer:
Order Service
→ publish OrderPlaced
→ broker
→ subscribers decide what to do
That shift is what reduces coupling.
The Order Service does not need to know that a new Fraud Detection Service was added yesterday. The Fraud Detection Service can subscribe to OrderPlaced and start reacting without changing the Order Service.
The Order Service does not need to wait for the Analytics Service to finish. Analytics can process the event independently.
The Order Service does not need to coordinate every subscriber deployment. Subscribers own their own implementation as long as they understand the event contract.
So the mental model is:
Publisher owns the event.
Broker owns delivery.
Subscribers own their reactions.
That is the heart of the Publisher/Subscriber pattern.
It gives you decoupling, fanout, and failure isolation, but it also gives you new design responsibilities: event contracts, idempotency, failure handling, and eventual consistency.
Where This Leads
You now understand the core idea behind the Publisher/Subscriber pattern.
Direct service calls are useful when a caller needs an immediate answer. But when a service only needs to announce that something happened, Pub/Sub gives you a cleaner shape.
The publisher emits an event.
The broker routes it.
Subscribers react independently.
That helps reduce runtime coupling, deployment coupling, and knowledge coupling between services.
But Pub/Sub also introduces a new problem: events still have to be processed reliably.
A subscriber may fail because a database is unavailable. A downstream API may return a temporary error. A network call may time out. A message may be malformed. A service may restart halfway through processing.
So the next design question becomes:
What should happen when event processing fails?
Should the subscriber retry immediately?
Should it wait and try again later?
When should it stop retrying?
How do you avoid overwhelming a struggling dependency with repeated attempts?
How do you separate temporary failures from messages that will never succeed?
That is where the next pattern comes in.
In the next article, we will look at the Retry pattern: how to handle transient failures without making the system worse.