Modern cloud applications rarely consist of a single service. A typical production system may contain APIs, background workers, databases, notification services, payment systems, analytics pipelines, and AI workloads.
When these services communicate directly with one another, the architecture can quickly become difficult to maintain.
For example:
Order API
|
+----> Email Service
|
+----> Inventory Service
|
+----> Payment Service
|
+----> Analytics Service
As more consumers are added, the number of connections increases.
An event-driven architecture provides another approach:
Order API
|
v
EventBridge Event Bus
|
+----> Inventory
+----> Email
+----> Analytics
+----> Notifications
Amazon EventBridge allows applications to publish events and route them to targets based on event rules. Custom event buses are especially useful when an application needs its own event-routing boundary instead of putting every event onto a shared default bus.
This article explains how custom EventBridge event buses work, how to design reliable event-driven applications with them, and how to integrate them with .NET applications.
What Is an Event Bus?
An event bus is a logical destination where applications publish events.
An event represents something that happened.
Examples include:
OrderCreated
PaymentCompleted
CustomerRegistered
InvoiceGenerated
DocumentProcessed
AgentCompleted
The producer does not need to know which services consume the event.
For example:
Order Service
|
| OrderCreated
v
Event Bus
|
+----> Inventory
+----> Email
+----> Analytics
This creates loose coupling between services.
The order service only needs to publish OrderCreated.
It does not need direct dependencies on inventory, email, or analytics services.
Default Event Bus vs Custom Event Bus
EventBridge provides a default event bus, but applications can also create custom event buses.
A custom event bus provides a dedicated routing boundary for a particular application, domain, team, or integration scenario.
For example:
Default Event Bus
|
+---- Application A
+---- Application B
+---- Application C
A custom design could instead use:
Order Bus
|
+---- Inventory
+---- Shipping
+---- Billing
Customer Bus
|
+---- CRM
+---- Notifications
+---- Analytics
The appropriate structure depends on organizational and application boundaries.
Why Use Custom Event Buses?
Custom event buses can provide several architectural benefits.
Domain Separation
Different business domains can have separate event-routing boundaries.
Commerce Bus
Customer Bus
Billing Bus
AI Bus
This can make ownership and event management easier to understand.
Access Control
Permissions can be scoped around event buses.
A producer may have permission to publish to one bus without automatically gaining access to unrelated event infrastructure.
Cleaner Routing Rules
A custom bus can contain rules relevant to a particular workload.
For example:
Order Bus
OrderCreated
|
+----> Inventory Rule
+----> Notification Rule
+----> Analytics Rule
This reduces unrelated rules being mixed together.
EventBridge Architecture
A simplified EventBridge architecture looks like this:
Producer
|
v
Custom Event Bus
|
+----------+----------+
| | |
v v v
Rule A Rule B Rule C
| | |
v v v
Target A Target B Target C
The important components are:
Event producer — publishes an event.
Event bus — receives events.
Event rule — determines which events should be processed.
Target — receives matching events.
Targets can include AWS services and supported endpoints.
Event Structure
A useful event should contain enough information for consumers to understand what happened.
A conceptual event might look like:
{
"source": "orders",
"detail-type": "OrderCreated",
"detail": {
"orderId": "ORD-1042",
"customerId": "CUS-204",
"total": 149.99
}
}
The event should describe the business fact rather than dictate what consumers must do.
For example:
Good:
OrderCreated
Less useful:
SendEmailToCustomer
OrderCreated describes something that happened.
SendEmailToCustomer describes an implementation action.
Keeping events focused on business facts allows more consumers to use them later.
Creating a Custom Event Bus
The infrastructure can be created through AWS tooling or infrastructure-as-code.
Conceptually:
Custom Event Bus
Name:
orders-production
The application then publishes events to that bus.
A production environment should generally manage infrastructure declaratively rather than creating resources manually for every deployment.
Infrastructure-as-code tools such as AWS CloudFormation, AWS CDK, or Terraform can help maintain consistent environments.
Publishing Events From .NET
A .NET application can use the AWS SDK to publish EventBridge events.
A simplified example:
using Amazon.EventBridge;
using Amazon.EventBridge.Model;
using System.Text.Json;
public class OrderEventPublisher
{
private readonly IAmazonEventBridge eventBridge;
public OrderEventPublisher(
IAmazonEventBridge eventBridge)
{
this.eventBridge = eventBridge;
}
public async Task PublishOrderCreatedAsync(
string orderId,
string customerId,
decimal total,
CancellationToken cancellationToken)
{
var detail = new
{
orderId,
customerId,
total
};
var request = new PutEventsRequest
{
Entries =
[
new PutEventsRequestEntry
{
EventBusName = "orders-production",
Source = "orders",
DetailType = "OrderCreated",
Detail = JsonSerializer.Serialize(detail)
}
]
};
await eventBridge.PutEventsAsync(
request,
cancellationToken);
}
}
The producer only needs to know the event contract and destination bus.
It does not need to know which services will consume the event.
Registering the AWS Client in ASP.NET Core
The AWS SDK client can be registered with dependency injection.
builder.Services.AddAWSService<IAmazonEventBridge>();
Then inject the client into your event publishing service.
This keeps AWS infrastructure concerns outside controllers and business logic.
Event Rules
Publishing an event is only the first step.
Rules determine what should happen with matching events.
Suppose the bus receives:
OrderCreated
OrderCancelled
PaymentCompleted
ShipmentCreated
A rule can match only:
OrderCreated
and route it to a target.
Conceptually:
Event Bus
|
+---- OrderCreated ------> Inventory Lambda
|
+---- PaymentCompleted --> Receipt Service
|
+---- ShipmentCreated --> Notification Service
This allows multiple consumers to react independently.
Event Pattern Matching
Rules can filter events based on event attributes.
For example, a rule might match:
{
"source": ["orders"],
"detail-type": ["OrderCreated"]
}
Another rule could filter based on the contents of the detail.
For example:
{
"source": ["orders"],
"detail-type": ["OrderCreated"],
"detail": {
"total": [
{
"numeric": [">", 1000]
}
]
}
}
This allows different workflows to react to different types of events.
One Event, Multiple Consumers
One of the strongest characteristics of event-driven architecture is fan-out.
Consider:
OrderCreated
|
v
EventBridge
/ | \
/ | \
v v v
Inventory Email Analytics
The producer publishes the event once.
Multiple consumers can independently process it.
This avoids direct dependencies such as:
Order Service
|
+----> Inventory
|
+----> Email
|
+----> Analytics
The event bus becomes the integration boundary.
Reliability Considerations
Event-driven systems are asynchronous, which changes how reliability should be designed.
An HTTP request might return an immediate success or failure.
An event publication can succeed while downstream processing happens later.
Therefore, distinguish between:
Event accepted
|
v
Event delivered
|
v
Target processed
|
v
Business operation completed
These are different states.
A producer should not assume that publishing an event means every downstream operation has already completed.
Handling Failed Consumers
Suppose an event reaches a target but processing fails.
OrderCreated
|
v
Consumer
|
X
Failure
A reliable architecture needs a strategy for failures.
Depending on the target and architecture, this can include retries, dead-letter queues, error handling, and operational alerts.
A dead-letter queue can provide a place for events that could not be successfully processed after the configured delivery attempts.
Conceptually:
Event
|
v
Consumer
|
+---- Success ---> Done
|
+---- Failure
|
v
Retry
|
+---- Success
|
+---- Failure
|
v
DLQ
The exact retry and dead-letter behavior depends on the EventBridge target configuration and downstream service.
Idempotency Is Essential
Retries introduce a common problem:
The same event may be processed more than once.
Suppose:
OrderCreated
|
v
Consumer
|
+---- Process
|
+---- Timeout
The producer or delivery system may retry.
The consumer could receive the same logical event again.
If processing is not idempotent, the application might:
Charge customer
Charge customer again
or:
Create shipment
Create duplicate shipment
Consumers should therefore use an event identifier or another idempotency key.
For example:
public async Task HandleAsync(
string eventId,
CancellationToken cancellationToken)
{
if (await AlreadyProcessedAsync(eventId))
return;
await ProcessEventAsync(cancellationToken);
await MarkProcessedAsync(
eventId,
cancellationToken);
}
The storage mechanism should be designed so that concurrent processing cannot create duplicate effects.
Event Ordering
Developers often assume that events will always be processed in the same order in which they were created.
That assumption can create bugs.
Consider:
OrderCreated
OrderUpdated
OrderCancelled
A consumer may need to account for the possibility that events are observed or processed independently.
If strict ordering is a business requirement, design explicitly for it rather than assuming that a general event bus provides the ordering semantics your application needs.
Schema Evolution
Events are APIs.
Once multiple consumers depend on an event structure, changing it carelessly can break downstream systems.
For example, an initial event might contain:
{
"orderId": "ORD-100",
"total": 100
}
Later, the producer may need:
{
"orderId": "ORD-100",
"total": 100,
"currency": "USD"
}
Adding optional fields is generally easier for consumers to tolerate than removing or changing the meaning of existing fields.
Treat event contracts as versioned integration contracts.
Event Naming
Use consistent event names.
A practical convention is:
OrderCreated
OrderCancelled
PaymentCompleted
CustomerRegistered
InvoiceGenerated
Names should describe facts that have occurred.
Avoid vague names such as:
OrderEvent
ProcessOrder
DoSomething
Clear names make rules and consumers easier to understand.
Separating Business Events From Technical Events
Not every event needs to represent a business operation.
Technical events may also be useful:
ApplicationStarted
FileProcessed
JobCompleted
However, business events should remain focused on domain meaning.
For example:
PaymentCompleted
is more reusable than:
StripeWebhookReceived
The first describes the business fact.
The second exposes an implementation detail.
EventBridge and AI Applications
Event-driven architecture is also useful for AI applications.
An AI agent may produce events such as:
AgentStarted
ToolCalled
DocumentRetrieved
AgentCompleted
These events can be consumed by:
Monitoring systems
Audit services
Analytics pipelines
Notification systems
Workflow processors
For example:
AI Agent
|
v
AgentCompleted
|
v
EventBridge
/ | \
v v v
Audit Analytics Notification
This prevents the agent application from directly integrating with every downstream system.
Event-Driven Architecture vs Direct API Calls
Approach | Direct API | Event-Driven |
|---|---|---|
Communication | Synchronous | Asynchronous |
Coupling | Higher | Lower |
Immediate response | Yes | Usually no |
Fan-out | Requires orchestration | Natural |
Retry handling | Application-specific | Infrastructure-supported patterns |
Failure isolation | More limited | Stronger |
Workflow complexity | Can grow quickly | Better for independent consumers |
Debugging | Often straightforward | Requires event tracing |
Neither approach replaces the other.
Use direct APIs when the caller needs an immediate response.
Use events when the producer should notify one or more consumers without waiting for them to finish.
When Not to Use an Event Bus
Event-driven architecture is not automatically better.
A simple operation such as:
GetCustomerById()
may be better represented as a direct API call.
Introducing an event bus can add:
Operational complexity
Asynchronous behavior
Additional monitoring requirements
Event schema management
Retry handling
Debugging complexity
Use an event bus when asynchronous communication and decoupling provide a clear architectural benefit.
Monitoring Event-Driven Applications
Production systems should monitor:
Events published
Events matched
Target failures
Retry count
Dead-letter messages
Processing latency
Event age
Consumer health
A useful dashboard might contain:
Metric | Purpose |
|---|---|
Published events | Producer activity |
Failed invocations | Consumer reliability |
Retry count | Delivery problems |
DLQ messages | Events requiring investigation |
Processing duration | Consumer performance |
Event age | Processing backlog |
Distributed tracing can also help connect the original HTTP request to the events it produced and the downstream operations triggered by those events.
Security
Event buses should be treated as infrastructure boundaries.
Follow least-privilege access.
For example:
Order Service
|
+---- Publish ---> Order Bus
Inventory Service
|
+---- Consume ---> OrderCreated
The order service does not necessarily need permissions to consume unrelated events.
Likewise, consumers should only receive the events required for their responsibilities.
Sensitive event data should also be minimized.
Do not place secrets or unnecessary personal information into event payloads.
Common Mistakes
Creating One Giant Event Bus
A single bus can become difficult to govern when unrelated domains and teams share it.
Use logical boundaries that reflect application ownership and operational needs.
Making Events Too Large
Events should contain the information consumers actually need.
Avoid turning events into complete database records unless there is a clear reason.
Putting Commands Into Event Names
An event should generally describe what happened.
Prefer:
OrderCreated
over:
CreateOrder
Ignoring Duplicate Processing
Assume that consumers may need to handle duplicate delivery safely.
Design idempotency into important operations.
Ignoring Dead-Letter Events
A DLQ is not a permanent storage system.
Create operational processes for investigating and replaying failed events where appropriate.
Changing Event Contracts Without Planning
Consumers may depend on the existing schema.
Treat event schemas as integration contracts.
Best Practices
When building applications with Amazon EventBridge custom event buses:
Use events to represent meaningful facts.
Keep producers independent from consumers.
Use custom buses to establish clear application or domain boundaries.
Create focused event rules.
Design consumers to be idempotent.
Plan for retries and failed processing.
Monitor dead-letter events.
Treat event schemas as APIs.
Avoid unnecessary payload data.
Use least-privilege permissions.
Monitor event and target latency.
Use tracing and correlation identifiers for troubleshooting.
Do not assume event ordering unless the architecture explicitly guarantees it.
Use direct APIs when synchronous communication is the better fit.
Conclusion
Amazon EventBridge custom event buses provide a useful foundation for building loosely coupled, event-driven applications.
Instead of creating direct dependencies between every service, applications can publish business events to an event bus and allow independent consumers to react to them.
A well-designed architecture looks like this:
Producer
|
v
Custom Event Bus
|
+---- Rule ----> Consumer A
|
+---- Rule ----> Consumer B
|
+---- Rule ----> Consumer C
The real value comes from the architectural separation.
The producer does not need to know who consumes the event. Consumers can evolve independently, and new consumers can often be added without modifying the original producer.
For production systems, reliability requires more than publishing events. Teams should design for retries, duplicate delivery, idempotency, dead-letter handling, schema evolution, observability, and security.
Used appropriately, EventBridge can turn tightly connected service integrations into a more flexible event-driven architecture.
Summary
Custom EventBridge event buses provide an event-routing boundary between producers and consumers. They are useful when applications need asynchronous communication, fan-out, domain separation, and reduced coupling between services.
For .NET applications, the AWS SDK makes it possible to publish strongly structured events from ASP.NET Core services. The surrounding architecture should then focus on reliable consumers, clear event contracts, idempotent processing, failure handling, and operational visibility.
The key principle is simple:
Publish what happened.
Let independent consumers decide what to do next.
Join the conversation! Your thoughts help the community grow.