Integration events are how you share state across microservices without them talking directly to each other.
Let’s say you have a CustomerService and an OrderService. When a new customer is created in CustomerService, you want OrderService to know about it so it can start accepting orders for that customer. Instead of CustomerService making a direct API call to OrderService (which couples them tightly), CustomerService publishes an CustomerCreated integration event. OrderService (and any other service that cares) subscribes to this event and reacts.
Here’s a simplified example of how this might look in practice, using C# and a hypothetical event bus:
// In CustomerService
public class CustomerService
{
private readonly IEventBus _eventBus;
public CustomerService(IEventBus eventBus)
{
_eventBus = eventBus;
}
public void CreateCustomer(Customer customer)
{
// ... save customer to database ...
var customerCreatedEvent = new CustomerCreatedEvent
{
CustomerId = customer.Id,
CustomerName = customer.Name,
Email = customer.Email
};
_eventBus.Publish(customerCreatedEvent);
}
}
// In OrderService
public class OrderService
{
private readonly IEventBus _eventBus;
public OrderService(IEventBus eventBus)
{
_eventBus = eventBus;
}
public void SubscribeToEvents()
{
_eventBus.Subscribe<CustomerCreatedEvent>(HandleCustomerCreated);
}
private void HandleCustomerCreated(CustomerCreatedEvent customerCreatedEvent)
{
Console.WriteLine($"Received CustomerCreated event for customer ID: {customerCreatedEvent.CustomerId}");
// ... do something with the customer data, e.g., create a default customer profile in OrderService ...
}
}
The IEventBus is the backbone. It’s responsible for receiving published events and delivering them to all subscribed consumers. This could be a message queue like RabbitMQ or Kafka, or a cloud-native service like Azure Service Bus or AWS SNS/SQS.
The core problem integration events solve is decoupling. Services don’t need to know about each other’s existence, network addresses, or API contracts. CustomerService only knows how to publish CustomerCreatedEvent to the bus. OrderService only knows how to subscribe to CustomerCreatedEvent from the bus. If OrderService is down when CustomerCreatedEvent is published, the event bus (if designed for durability) will hold onto the message until OrderService is back online and can process it. This makes your system more resilient.
The levers you control are primarily around the event schema and the event bus configuration.
Event Schema:
- Versioning: As your services evolve, your events will too. You need a strategy for versioning your events. A common approach is to include a version number in the event name (e.g.,
CustomerCreatedEventV1,CustomerCreatedEventV2) or within the event payload itself. This ensures that older consumers can still process older event versions and new consumers can handle newer versions. - Content: What data do you include in the event? The principle of "event-driven design" suggests events should represent something that happened (a state change). They often carry enough data for a consumer to act without needing to query the originating service immediately. However, avoid sending too much data; only what’s necessary for the immediate reaction. If a consumer needs more context, they can make a direct, synchronous call to the originating service after processing the event, or subscribe to other relevant events.
- Idempotency: Consumers should be able to process the same event multiple times without adverse side effects. This is crucial because message delivery guarantees are often "at-least-once." A common pattern is to use a unique event ID and track which IDs have already been processed.
Event Bus Configuration:
- Durability: How does the bus handle message persistence? If the bus goes down, are messages lost? Ensure your bus is configured for durability if guaranteed delivery is important.
- Delivery Guarantees: Does the bus offer "at-least-once," "at-most-once," or "exactly-once" delivery? Most managed queues offer "at-least-once." "Exactly-once" is complex and often achieved through a combination of bus features and careful consumer implementation.
- Topic/Queue Naming: A clear naming convention for topics or queues (e.g.,
customer.events.created,order.commands.place) helps manage complexity.
When you publish an event, it’s essentially a message containing a description of something that happened. The event bus acts as a central broker, delivering this message to any service that has registered its interest in that specific type of event. This decoupling means that CustomerService doesn’t need to know who is listening, only that it should announce the CustomerCreated event. OrderService doesn’t need to know who published the event, only that it’s interested in CustomerCreated events. The event bus handles the routing.
The most surprising thing about integration events is that they often contain all the data needed for the consumer to perform its action, even if that data is also stored in the originating service. This "denormalization" in the event payload is a feature, not a bug, and it’s what allows consumers to react quickly and independently without immediately needing to query the source service. It’s a form of controlled data duplication that enhances responsiveness and resilience, allowing services to operate even if the originating service is temporarily unavailable.
The next logical step is to consider how these events can be used to trigger commands in other services, leading to a more complex choreography of microservice interactions.