DEV Community

Dinesh Dunukedeniya
Dinesh Dunukedeniya

Posted on • Edited on

Event Collaboration in Event-Driven Architecture (EDA)

What is Event Collaboration in EDA?

In simple terms, Event Collaboration/ Choreography is a decentralized way for services to communicate through events without a central controller. Think of it like a dance where each participant knows their steps and reacts based on cues from others there’s no single conductor telling everyone what to do.

In the context of EDA:

  • Service emit events upon state changes, enabling others to listen and react accordingly.

  • Each service listens for relevant events and reacts accordingly.

  • There is no central orchestrator managing the process.

  • Each microservice typically maintains its own state based on events it receives.

This pattern is sometimes also called Broker Topology, a term popularized by Mark Richards in Software Architecture Patterns (O’Reilly).
The name highlights that services communicate indirectly through a message broker, which routes events but does not control the workflow.

How Does Event Collaboration Work?

Imagine an online store scenario:

Order Service emits an OrderPlaced event.

  • Inventory Service listens to OrderPlaced, checks stock, and emits StockReserved or StockOut.

  • Payment Service listens to StockReserved, processes payment, and emits PaymentCompleted.

  • Shipping Service listens to PaymentCompleted and arranges delivery.

Each service acts independently, reacting to events and emitting new events for others. The flow emerges naturally from these interactions.

Choreography

When to Use Event Collaboration in Event-Driven Architecture

Event coloboration is a powerful architectural pattern, but it’s not the perfect fit for every scenario. Consider using choreography when:

✅ You Want Loose Coupling Between Services
Each service operates independently, only knowing about events, not about the other services. This reduces dependencies and allows teams to work autonomously.

✅ Your System Is Highly Distributed
In microservices or cloud-native architectures where services are deployed separately, choreography supports scalability and flexibility.

✅ You Need Asynchronous, Event-Driven Workflows
If your business processes naturally fit event streams (e.g., order processing, payment events), choreography enables responsive and resilient flow

✅ You Expect to Scale or Evolve Services Independently
Since services produce and consume events without tight integration, you can add or update services with minimal impact on others.

✅ You Can Invest in Observability and Monitoring
Because choreography lacks a central controller, you need good tooling for tracing, logging, and monitoring event flows across services.

Avoid

⚠️ If you require strict, centralized control over business workflows, orchestration might be a better fit.

⚠️ When transactional consistency is critical across services and must be tightly managed.

⚠️ If your team is new to distributed event-driven systems and you want simpler debugging and error handling initially.

Benefits of Event Collaboration

  • Loose Coupling: Services only know about event formats, not about each other directly. This simplifies changes and scaling.

  • Scalability: Services can be developed, deployed, and scaled independently.

  • Flexibility: New services can join by simply subscribing to events.

  • Resilience: Failure in one service doesn’t block the whole process.

Challenges to Consider

  • Visibility & Monitoring: Without a central controller, tracking the overall process flow requires dedicated observability tools.

  • Complexity in Debugging: Understanding event chains can be tricky when many services interact asynchronously.

  • Consistency: Ensuring data consistency across services can be more challenging without orchestration.

Implementing the Choreography Pattern in .NET Microservices with Kafka

In a choreography-based event-driven system, each microservice reacts to events and, if needed, emits its own events. This allows services to be highly autonomous and loosely coupled.

Since all microservices are ASP.NET Core web apps, Kafka event consumers are implemented using BackgroundService (hosted services) within each app.

Architecture Overview

Microservices Involved:

  • Order Service
    Initiates the workflow by publishing OrderPlaced events.

  • Inventory Service
    Listens for OrderPlaced events, checks stock, and publishes either InventoryReserved or InventoryReservationFailed.

  • Payment Service
    Listens for InventoryReserved and processes payment, publishing PaymentProcessed or PaymentFailed.

  • Shipping Service
    Listens for PaymentProcessed events and handles shipping.

Each Microservice Includes:

  • Kafka Consumer Background Service
    Subscribes to relevant Kafka topics and handles incoming events.

  • Kafka Producer Service
    Publishes events to Kafka topics to trigger downstream service actions.

  • Controller/Endpoints (Optional)
    For internal testing, admin access, or debug operations.

  • Domain Models (Shared)
    All services reference a shared contracts library to ensure consistent event models.

public class OrderPlaced
{
    public string OrderId { get; set; }
    public string ProductId { get; set; }
    public int Quantity { get; set; }
    public DateTime OrderDate { get; set; }
}

public class InventoryReserved
{
    public string OrderId { get; set; }
}

public class InventoryReservationFailed
{
    public string OrderId { get; set; }
    public string Reason { get; set; }
}

public class PaymentProcessed
{
    public string OrderId { get; set; }
}

public class PaymentFailed
{
    public string OrderId { get; set; }
    public string Reason { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
  • Kafka Producer Service Each service needs a way to publish events to Kafka. This reusable service wraps the Kafka producer logic.
// KafkaProducerService.cs
public interface IKafkaProducerService
{
    Task ProduceAsync<T>(string topic, T message);
}

public class KafkaProducerService : IKafkaProducerService
{
    private readonly IProducer<Null, string> _producer;

    public KafkaProducerService(IConfiguration configuration)
    {
        var config = new ProducerConfig
        {
            BootstrapServers = configuration["Kafka:BootstrapServers"]
        };
        _producer = new ProducerBuilder<Null, string>(config).Build();
    }

    public async Task ProduceAsync<T>(string topic, T message)
    {
        var json = JsonSerializer.Serialize(message);
        await _producer.ProduceAsync(topic, new Message<Null, string> { Value = json });
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Kafka Consumer BackgroundService Each service implements a BackgroundService that listens for specific events.

Example for InventoryService consuming OrderPlaced:

// OrderPlacedConsumer.cs
public class OrderPlacedConsumer : BackgroundService
{
    private readonly IConfiguration _configuration;
    private readonly IKafkaProducerService _producer;

    public OrderPlacedConsumer(IConfiguration configuration, IKafkaProducerService producer)
    {
        _configuration = configuration;
        _producer = producer;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var config = new ConsumerConfig
        {
            BootstrapServers = _configuration["Kafka:BootstrapServers"],
            GroupId = "inventory-service",
            AutoOffsetReset = AutoOffsetReset.Earliest
        };

        using var consumer = new ConsumerBuilder<Ignore, string>(config).Build();
        consumer.Subscribe("order-placed");

        while (!stoppingToken.IsCancellationRequested)
        {
            var result = consumer.Consume(stoppingToken);
            var order = JsonSerializer.Deserialize<OrderPlaced>(result.Message.Value);

            // Simulated logic
            var isAvailable = true; // simulate stock check

            if (isAvailable)
            {
                await _producer.ProduceAsync("inventory-reserved", new InventoryReserved { OrderId = order.OrderId });
            }
            else
            {
                await _producer.ProduceAsync("inventory-failed", new InventoryReservationFailed
                {
                    OrderId = order.OrderId,
                    Reason = "Out of stock"
                });
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Payment Service Consumer
public class InventoryReservedConsumer : BackgroundService
{
    private readonly IKafkaProducerService _producer;
    private readonly IConfiguration _configuration;

    public InventoryReservedConsumer(IKafkaProducerService producer, IConfiguration configuration)
    {
        _producer = producer;
        _configuration = configuration;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var config = new ConsumerConfig
        {
            BootstrapServers = _configuration["Kafka:BootstrapServers"],
            GroupId = "payment-service",
            AutoOffsetReset = AutoOffsetReset.Earliest
        };

        using var consumer = new ConsumerBuilder<Ignore, string>(config).Build();
        consumer.Subscribe("inventory-reserved");

        while (!stoppingToken.IsCancellationRequested)
        {
            var result = consumer.Consume(stoppingToken);
            var evt = JsonSerializer.Deserialize<InventoryReserved>(result.Message.Value);

            // Simulated payment logic
            var success = true;

            if (success)
            {
                await _producer.ProduceAsync("payment-processed", new PaymentProcessed { OrderId = evt.OrderId });
            }
            else
            {
                await _producer.ProduceAsync("payment-failed", new PaymentFailed
                {
                    OrderId = evt.OrderId,
                    Reason = "Card declined"
                });
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

Event Collaboration in EDA promotes a scalable, flexible, and resilient architecture by allowing services to communicate asynchronously and independently through events. While it introduces complexity in monitoring and debugging, with the right tools and practices, choreography can be a powerful pattern for modern distributed systems.

Top comments (0)