Przejdź do treści głównej

Event-Driven Architecture Patterns 2025

Kompleksowy przewodnik po Event-Driven Architecture: Event Sourcing, CQRS, Saga pattern, message brokers (Kafka, RabbitMQ, Azure Service Bus). Praktyczne wzorce EDA w microservices z rzeczywistym case study e-commerce.

Autor: Michał Wojciechowski··16 min czytania
Event-driven architecture with flowing data streams

Dlaczego Event-Driven Architecture rewolucjonizuje systemy w 2025?

Wyobraź sobie system, który reaguje na zmiany w czasie rzeczywistym, automatycznie skaluje się pod obciążeniem i nigdy nie traci ani jednej informacji o tym, co się wydarzyło.

To właśnie daje Ci Event-Driven Architecture (EDA) – wzorzec architektoniczny, gdzie komponenty komunikują się poprzez eventy zamiast tradycyjnych wywołań bezpośrednich. W praktyce oznacza to, że Twoje serwisy nie muszą czekać na siebie nawzajem, mogą pracować równolegle i niezależnie.

Liczby mówią same za siebie. Według raportu Gartner 2024, już 60% aplikacji enterprise'owych wykorzystuje wzorce event-driven. Giganci jak Amazon, Netflix, Uber czy Spotify opierają swoje systemy właśnie na eventach.

Dlaczego? Ponieważ EDA rozwiązuje trzy fundamentalne problemy rozproszonych systemów: zbyt silne powiązania między serwisami (coupling), ograniczoną skalowalność i niską odporność na awarie (resilience). Jeśli Twój system przetwarza więcej niż kilkaset requestów na sekundę, EDA przestaje być "nice to have" a staje się koniecznością.

W tym kompleksowym przewodniku pokażę Ci praktyczne wzorce EDA z prawdziwymi przykładami kodu. Dowiesz się jak zaimplementować Event Sourcing, CQRS i Saga pattern. Porównamy najpopularniejsze message brokery (Apache Kafka, RabbitMQ, Azure Service Bus) i zobaczysz kompletny case study platformy e-commerce. Poznasz asynchroniczną komunikację, event streaming i wzorce integracji dla systemów rozproszonych. Jeśli rozważasz migrację do architektury mikrousług, zrozumienie EDA jest absolutnie kluczowe. Zobacz też nasze API integrations guide dla pełnego kontekstu integracji systemów.

Kluczowe koncepcje EDA 2025:

  • Event Sourcing – przechowywanie state jako sequence of immutable events
  • CQRS – separacja read i write models dla optimized queries
  • Message Brokers – Kafka, RabbitMQ, Azure Service Bus dla reliable delivery
  • Saga Pattern – distributed transactions w microservices
  • Event Streaming – real-time processing dla millions events/second

Podstawy Event-Driven Architecture - Jak to działa w praktyce?

Zanim przejdziemy do zaawansowanych wzorców, zrozummy fundamenty.

Event-Driven Architecture opiera się na trzech kluczowych elementach: Events (eventy), Event Producers (producenci) i Event Consumers (konsumenci). Event to niezmienialny fakt (immutable fact) reprezentujący coś, co rzeczywiście się wydarzyło w systemie – na przykład "zamówienie zostało złożone" czy "płatność została przetworzona".

Event Structure

Event zawiera metadata i payload opisujący co się wydarzyło:

{
  "eventId": "evt_123456",
  "eventType": "OrderPlaced",
  "timestamp": "2025-11-22T10:30:00Z",
  "aggregateId": "order_789",
  "version": 1,
  "data": {
    "customerId": "cust_456",
    "orderTotal": 299.99,
    "items": [
      {
        "productId": "prod_123",
        "quantity": 2,
        "price": 149.99
      }
    ]
  },
  "metadata": {
    "correlationId": "corr_abc",
    "causationId": "evt_000"
  }
}

Producers & Consumers

Producers emitują events, consumers reagują na nie:

  • Producer: Order Service publikuje OrderPlaced event
  • Consumer 1: Inventory Service rezerwuje stock
  • Consumer 2: Payment Service procesuje płatność
  • Consumer 3: Notification Service wysyła email
  • Consumer 4: Analytics Service zapisuje do warehouse

Loose coupling: Producer nie wie o consumers. Dodanie nowego consumer nie wymaga zmian w producer.

EDA vs Request-Response

AspektRequest-ResponseEvent-Driven
CouplingTight - caller zna calleeLoose - producer nie zna consumers
SynchronicitySynchronous blockingAsynchronous non-blocking
ScalabilityLimited - caller czekaHigh - parallel processing
ResilienceCascade failuresIsolated failures, retry mechanisms
ExtensibilityAdding endpoint needs coordinationNew consumers bez zmian w producers

Kiedy stosować EDA?

Idealne zastosowania: Przetwarzanie zamówień w e-commerce, dane z czujników IoT, systemy tradingowe, śledzenie aktywności użytkowników, komunikacja między mikrousługami, analityka real-time, systemy notyfikacji, integracje zewnętrznych API.

Unikaj EDA gdy: Budujesz prostą aplikację CRUD, potrzebujesz natychmiastowej silnej spójności (strong consistency), zespół nie ma doświadczenia z systemami rozproszonymi, lub gdy złożoność debugowania jest nieakceptowalna dla Twojego biznesu.

Data flow and event processing visualization

Event Sourcing - Pełna historia zamiast tylko aktualnego stanu

Tradycyjne bazy danych przechowują tylko aktualny stan – "zamówienie ma status: wysłane". Gdy aktualizujesz rekord, poprzednia wartość znika bezpowrotnie.

Event Sourcing odwraca tę logikę. Zamiast przechowywać obecny stan, zapisujesz pełną sekwencję eventów – wszystkie zmiany, które doprowadziły do obecnego stanu. Każda zmiana to niezmienialny event. Aktualny stan odtwarzasz poprzez "odtworzenie" wszystkich eventów od początku. To jak time-travel dla Twojej aplikacji – w każdej chwili możesz zobaczyć dokładnie, co się wydarzyło i dlaczego.

Traditional State Storage vs Event Sourcing

Traditional (CRUD)

// Database row - current state only
{
  "orderId": "order_789",
  "status": "Shipped",
  "total": 299.99,
  "updatedAt": "2025-11-22T14:00:00Z"
}

// Lost information:
// - Kiedy order został placed?
// - Czy był modified?
// - Kto zatwierdził payment?
// - History nieodwracalnie utracona

Event Sourcing

// Event stream - full history
[
  {
    "type": "OrderPlaced",
    "timestamp": "2025-11-22T10:00:00Z",
    "data": { "total": 299.99 }
  },
  {
    "type": "PaymentProcessed",
    "timestamp": "2025-11-22T10:05:00Z",
    "data": { "amount": 299.99 }
  },
  {
    "type": "OrderShipped",
    "timestamp": "2025-11-22T14:00:00Z",
    "data": { "trackingId": "TR123" }
  }
]

// Current state = replay events
// Full audit trail preserved

Event Sourcing w praktyce - implementacja w .NET

Zobaczmy jak wygląda implementacja aggregate z Event Sourcing w .NET. Ten kod pokazuje kluczowe elementy: command handler, event handler i mechanizm replay:

public class Order : AggregateRoot
{
    public Guid OrderId { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal Total { get; private set; }
    private List<OrderItem> _items = new();

    // Command handler
    public void PlaceOrder(Guid customerId, List<OrderItem> items)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Order already placed");

        var @event = new OrderPlacedEvent
        {
            OrderId = Guid.NewGuid(),
            CustomerId = customerId,
            Items = items,
            Total = items.Sum(i => i.Price * i.Quantity),
            Timestamp = DateTime.UtcNow
        };

        ApplyEvent(@event); // Apply & store event
    }

    // Event handler - mutates state
    private void Apply(OrderPlacedEvent @event)
    {
        OrderId = @event.OrderId;
        _items = @event.Items;
        Total = @event.Total;
        Status = OrderStatus.Placed;
    }

    // Replay from event stream
    public static Order FromEvents(IEnumerable<DomainEvent> events)
    {
        var order = new Order();
        foreach (var @event in events)
        {
            order.ApplyEvent(@event, isNew: false);
        }
        return order;
    }
}

Event Store Schema

Event store to append-only log events per aggregate:

-- Event Store table
CREATE TABLE EventStore (
    EventId UNIQUEIDENTIFIER PRIMARY KEY,
    AggregateId UNIQUEIDENTIFIER NOT NULL,
    AggregateType NVARCHAR(255) NOT NULL,
    EventType NVARCHAR(255) NOT NULL,
    EventData NVARCHAR(MAX) NOT NULL, -- JSON
    Version INT NOT NULL,
    Timestamp DATETIME2 NOT NULL,
    Metadata NVARCHAR(MAX),

    CONSTRAINT UQ_Aggregate_Version
        UNIQUE (AggregateId, Version)
);

CREATE INDEX IX_AggregateId
    ON EventStore(AggregateId, Version);

-- Sample query: Load aggregate
SELECT EventType, EventData, Version
FROM EventStore
WHERE AggregateId = 'order_789'
ORDER BY Version ASC;

Snapshots - Optymalizacja wydajności

Co jeśli aggregate ma 10,000 eventów? Odtwarzanie ich wszystkich przy każdym odczycie byłoby wolne. Rozwiązanie: snapshoty. Zapisujesz snapshot stanu co N eventów (np. co 100), a później odtwarzasz tylko eventy po ostatnim snaphocie:

public class SnapshotStore
{
    // Save snapshot co N events (np. co 100)
    public async Task SaveSnapshot(
        Guid aggregateId,
        object state,
        int version)
    {
        await _db.Snapshots.AddAsync(new Snapshot
        {
            AggregateId = aggregateId,
            State = JsonSerializer.Serialize(state),
            Version = version,
            Timestamp = DateTime.UtcNow
        });
    }

    // Load: snapshot + events after snapshot
    public async Task<Order> LoadAggregate(Guid orderId)
    {
        // 1. Load latest snapshot
        var snapshot = await _db.Snapshots
            .Where(s => s.AggregateId == orderId)
            .OrderByDescending(s => s.Version)
            .FirstOrDefaultAsync();

        Order order;
        int fromVersion;

        if (snapshot != null)
        {
            order = JsonSerializer
                .Deserialize<Order>(snapshot.State);
            fromVersion = snapshot.Version + 1;
        }
        else
        {
            order = new Order();
            fromVersion = 1;
        }

        // 2. Replay events after snapshot
        var events = await _eventStore
            .GetEvents(orderId, fromVersion);

        foreach (var @event in events)
        {
            order.ApplyEvent(@event);
        }

        return order;
    }
}

// Result: 10,000 events → 100 events replay
// (snapshot at 9,900 + 100 new events)

Event Sourcing Benefits & Trade-offs

Benefits:

  • • Complete audit trail - compliance, debugging
  • • Temporal queries - state w dowolnym momencie
  • • Event replay - bug fixes, analytics
  • • No data loss - wszystkie zmiany preserved
  • • Natural fit dla EDA patterns

Trade-offs:

  • • Learning curve - mindset shift
  • • Event schema evolution complexity
  • • Query performance - replay overhead (mitigated przez snapshots)
  • • Storage growth - więcej danych niż CRUD
  • • Eventual consistency challenges

CQRS - Rozdziel operacje zapisu od odczytu

Czy zastanawiałeś się kiedyś, dlaczego ten sam model danych używasz zarówno do zapisywania jak i odczytywania?

CQRS (Command Query Responsibility Segregation) to wzorzec, który rozdziela model zapisu (commands) od modelu odczytu (queries). Commands mutują stan systemu, natomiast queries czytają zoptymalizowane, zdenormalizowane projekcje danych. To jak mieć dwie różne bazy danych – jedną zoptymalizowaną pod kątem zapisów i logiki biznesowej, drugą pod szybkie odczyty i wyświetlanie danych. CQRS w połączeniu z Event Sourcing tworzy niesamowicie potężną kombinację dla skalowalnych systemów.

CQRS Architecture

┌─────────────┐         ┌──────────────────┐
│   Client    │         │   Command Side   │
│             │─Command→│                  │
│  (UI/API)   │         │  - Validation    │
│             │         │  - Business Logic│
│             │         │  - Event Store   │
└─────────────┘         └──────────────────┘
      │                          │
      │                     Events │
      │                          ↓
      │                 ┌─────────────────┐
      │                 │  Event Stream   │
      │                 │  (Kafka/etc)    │
      │                 └─────────────────┘
      │                          │
      │                     Events │
      │                          ↓
      │                 ┌─────────────────┐
      │                 │  Projections    │
      │                 │  - Read Model 1 │
      │                 │  - Read Model 2 │
      │                 │  - Read Model N │
      ↓                 └─────────────────┘
┌─────────────┐                  │
│ Query Side  │←─────────────────┘
│             │
│ - Optimized │
│ - Cached    │
│ - Fast Read │
└─────────────┘

Command Side Implementation

Commands reprezentują intencję zmiany state:

// Command
public record PlaceOrderCommand(
    Guid CustomerId,
    List<OrderItem> Items
) : ICommand;

// Command Handler
public class PlaceOrderCommandHandler
    : ICommandHandler<PlaceOrderCommand>
{
    private readonly IEventStore _eventStore;
    private readonly IEventBus _eventBus;

    public async Task<Result> Handle(
        PlaceOrderCommand command)
    {
        // 1. Validate
        if (!command.Items.Any())
            return Result.Failure("No items");

        // 2. Create aggregate
        var order = new Order();
        order.PlaceOrder(
            command.CustomerId,
            command.Items
        );

        // 3. Save events
        var events = order.GetUncommittedEvents();
        await _eventStore.SaveEvents(
            order.OrderId,
            events
        );

        // 4. Publish events
        foreach (var @event in events)
        {
            await _eventBus.Publish(@event);
        }

        return Result.Success(order.OrderId);
    }
}

Query Side - Projections

Projections budują optimized read models z event stream:

// Projection: OrderSummaryReadModel
public class OrderSummaryProjection
{
    private readonly IReadModelStore _readStore;

    // Event handler - builds read model
    public async Task Handle(OrderPlacedEvent @event)
    {
        var summary = new OrderSummaryReadModel
        {
            OrderId = @event.OrderId,
            CustomerId = @event.CustomerId,
            Total = @event.Total,
            Status = "Placed",
            PlacedAt = @event.Timestamp,
            ItemCount = @event.Items.Count
        };

        await _readStore.Save(summary);
    }

    public async Task Handle(OrderShippedEvent @event)
    {
        var summary = await _readStore
            .Get<OrderSummaryReadModel>(@event.OrderId);

        summary.Status = "Shipped";
        summary.ShippedAt = @event.Timestamp;

        await _readStore.Update(summary);
    }
}

// Read Model - denormalized, optimized for queries
public class OrderSummaryReadModel
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public string CustomerName { get; set; } // Denormalized
    public decimal Total { get; set; }
    public string Status { get; set; }
    public int ItemCount { get; set; }
    public DateTime PlacedAt { get; set; }
    public DateTime? ShippedAt { get; set; }
}

// Query - fast, no joins needed
public class OrderQueryService
{
    private readonly IReadModelStore _readStore;

    public async Task<OrderSummaryReadModel>
        GetOrderSummary(Guid orderId)
    {
        // Direct lookup, no aggregate replay
        return await _readStore
            .Get<OrderSummaryReadModel>(orderId);
    }

    public async Task<List<OrderSummaryReadModel>>
        GetCustomerOrders(Guid customerId)
    {
        // Optimized query, indexed read model
        return await _readStore
            .Query<OrderSummaryReadModel>()
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.PlacedAt)
            .ToListAsync();
    }
}

Multiple Projections - Different Use Cases

Jeden event stream → wiele specialized read models:

  • OrderSummaryProjection: UI list view, dashboards
  • OrderDetailsProjection: Detail page z pełnymi informacjami
  • CustomerOrderHistoryProjection: Customer-specific queries
  • AnalyticsProjection: Data warehouse, reports
  • SearchProjection: Elasticsearch index dla full-text search

Każdy projection może używać innej database technology optymalnej dla use case (SQL, NoSQL, Search, Cache).

CQRS Complexity Spectrum

Simple CQRS: Shared database, separate models. CQRS + Event Sourcing: Event store dla commands, projections dla queries. Full CQRS: Separate databases, eventual consistency, multiple specialized projections. Start simple, ewoluuj w miarę potrzeb. Nie każdy system potrzebuje full CQRS.

Network communication and message flow

Message Brokers - Który wybrać: Kafka, RabbitMQ czy Azure Service Bus?

Message broker to serce każdego systemu event-driven. To on zapewnia niezawodne dostarczanie eventów między producentami a konsumentami.

Wybór odpowiedniego message brokera może zadecydować o sukcesie lub porażce całej architektury. Kafka świetnie sprawdza się przy milionach eventów na sekundę, RabbitMQ dominuje w tradycyjnych kolejkach zadań, a Azure Service Bus jest naturalnym wyborem dla ekosystemu Azure. Przyjrzyjmy się każdemu z nich i zobaczmy, który pasuje do Twoich potrzeb.

Kafka - Event Streaming Platform

Apache Kafka to distributed event streaming platform dla high-throughput, low-latency scenarios:

Key Features:

  • • Throughput: miliony messages/second
  • • Event retention: dni/tygodnie (configurable)
  • • Event replay: consumers mogą re-read history
  • • Partitioning: parallel processing, ordering w partition
  • • Exactly-once semantics (transactions)
  • • Kafka Streams: stream processing

Best For:

  • • Event Sourcing event store
  • • Real-time analytics pipelines
  • • Log aggregation (aplikacje, metrics)
  • • IoT sensor data processing
  • • Microservices event bus
  • • CDC (Change Data Capture)
// Kafka Producer (.NET)
var config = new ProducerConfig
{
    BootstrapServers = "kafka:9092",
    Acks = Acks.All // Wait for all replicas
};

using var producer = new ProducerBuilder<string, string>(config)
    .Build();

var message = new Message<string, string>
{
    Key = order.OrderId.ToString(),
    Value = JsonSerializer.Serialize(orderPlacedEvent)
};

await producer.ProduceAsync("orders", message);

// Kafka Consumer
var config = new ConsumerConfig
{
    BootstrapServers = "kafka:9092",
    GroupId = "order-processor",
    AutoOffsetReset = AutoOffsetReset.Earliest
};

using var consumer = new ConsumerBuilder<string, string>(config)
    .Build();

consumer.Subscribe("orders");

while (true)
{
    var result = consumer.Consume(cancellationToken);
    var @event = JsonSerializer
        .Deserialize<OrderPlacedEvent>(result.Message.Value);

    await ProcessEvent(@event);
    consumer.Commit(result); // Manual commit
}

RabbitMQ - Traditional Message Broker

RabbitMQ to message queue z flexible routing, ideal dla traditional messaging patterns:

Key Features:

  • • Routing: exchanges, queues, bindings
  • • Message TTL, dead-letter queues
  • • Priority queues
  • • Consumer acknowledgements
  • • Plugin ecosystem (shovel, federation)
  • • AMQP, MQTT, STOMP protocols

Best For:

  • • Task queues (background jobs)
  • • Request-response patterns
  • • RPC (Remote Procedure Call)
  • • Complex routing scenarios
  • • Lower throughput (<100k msg/sec)
  • • Transient messages (nie event store)
// RabbitMQ Publisher (.NET)
var factory = new ConnectionFactory
{
    HostName = "rabbitmq",
    UserName = "user",
    Password = "pass"
};

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

// Declare exchange & queue
channel.ExchangeDeclare(
    exchange: "orders",
    type: ExchangeType.Topic,
    durable: true
);

channel.QueueDeclare(
    queue: "order.placed",
    durable: true,
    exclusive: false,
    autoDelete: false
);

channel.QueueBind(
    queue: "order.placed",
    exchange: "orders",
    routingKey: "order.placed"
);

// Publish message
var body = Encoding.UTF8.GetBytes(
    JsonSerializer.Serialize(orderPlacedEvent)
);

var properties = channel.CreateBasicProperties();
properties.Persistent = true;

channel.BasicPublish(
    exchange: "orders",
    routingKey: "order.placed",
    basicProperties: properties,
    body: body
);

// Consumer
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
    var body = ea.Body.ToArray();
    var message = Encoding.UTF8.GetString(body);
    var @event = JsonSerializer
        .Deserialize<OrderPlacedEvent>(message);

    ProcessEvent(@event);

    channel.BasicAck(ea.DeliveryTag, false);
};

channel.BasicConsume(
    queue: "order.placed",
    autoAck: false,
    consumer: consumer
);

Azure Service Bus - Managed Cloud Service

Azure Service Bus to fully managed message broker z enterprise features:

Key Features:

  • • Fully managed (no infrastructure)
  • • Topics & Subscriptions (pub/sub)
  • • Sessions (ordered processing)
  • • Dead-letter queues built-in
  • • Duplicate detection
  • • Azure integration (Managed Identity)

Best For:

  • • Azure-native applications
  • • Enterprise messaging
  • • Managed service preference
  • • Medium throughput needs
  • • .NET ecosystem
  • • Zero infrastructure management
// Azure Service Bus Sender (.NET)
var client = new ServiceBusClient(
    "Endpoint=sb://namespace.servicebus.windows.net/...",
    new DefaultAzureCredential() // Managed Identity
);

var sender = client.CreateSender("orders");

var message = new ServiceBusMessage(
    JsonSerializer.Serialize(orderPlacedEvent)
)
{
    ContentType = "application/json",
    MessageId = orderPlacedEvent.EventId.ToString(),
    SessionId = orderPlacedEvent.OrderId.ToString()
};

await sender.SendMessageAsync(message);

// Receiver
var processor = client.CreateProcessor(
    "orders",
    new ServiceBusProcessorOptions
    {
        AutoCompleteMessages = false,
        MaxConcurrentCalls = 10
    }
);

processor.ProcessMessageAsync += async args =>
{
    var body = args.Message.Body.ToString();
    var @event = JsonSerializer
        .Deserialize<OrderPlacedEvent>(body);

    await ProcessEvent(@event);

    // Complete message (remove from queue)
    await args.CompleteMessageAsync(args.Message);
};

processor.ProcessErrorAsync += args =>
{
    Console.WriteLine(args.Exception);
    return Task.CompletedTask;
};

await processor.StartProcessingAsync();

Broker Comparison Table

FeatureKafkaRabbitMQAzure Service Bus
ThroughputBardzo wysoki (miliony/sec)Średni (100k/sec)Średni
Event RetentionDni/tygodnie (configurable)Transient (consumed = deleted)Transient (TTL available)
Event ReplayTak (offset reset)NieNie
OrderingPer partitionPer queueSessions (guaranteed)
ManagementSelf-hosted / Confluent CloudSelf-hosted / CloudAMQPFully managed (Azure)
KosztInfrastructure + licensingLow (open source)Pay-per-operation
Best Use CaseEvent streaming, Event SourcingTask queues, RPCAzure-native apps

Jak wybrać odpowiedni broker?

Wybierz Kafka gdy: Implementujesz Event Sourcing, potrzebujesz bardzo wysokiego throughput (miliony msg/sec), musisz mieć możliwość odtwarzania eventów (replay), lub wymagane jest długoterminowe przechowywanie eventów.

Wybierz RabbitMQ gdy: Budujesz kolejki zadań (task queues), potrzebujesz złożonego routingu wiadomości, implementujesz wzorce RPC, lub throughput poniżej 100k msg/sec jest wystarczający.

Wybierz Azure Service Bus gdy: Pracujesz w ekosystemie Azure, preferujesz w pełni zarządzany serwis (zero operacji infrastrukturalnych), lub potrzebujesz enterprise features jak sessions czy dead-letter queues. Wiele firm stosuje też podejście hybrydowe: Kafka do event streaming, RabbitMQ do kolejek zadań.

Saga Pattern - Transakcje rozproszone bez bólu głowy

W architekturze mikrousług najtrudniejszym wyzwaniem są transakcje rozpięte na wiele serwisów. Tradycyjne ACID transactions po prostu nie działają w systemach rozproszonych.

Tu z pomocą przychodzi Saga pattern. Implementuje on distributed transactions jako sekwencję lokalnych transakcji. Każdy krok publikuje event po zakończeniu. Jeśli coś pójdzie nie tak, kompensujące transakcje (compensating transactions) cofają poprzednie kroki. To eleganckie rozwiązanie problemu, który wcześniej wydawał się nierozwiązywalny.

Saga Choreography vs Orchestration

Choreography (Decentralized)

Order Service
  ↓ OrderPlaced event
Payment Service
  → PaymentProcessed event
  ↓
Inventory Service
  → StockReserved event
  ↓
Shipping Service
  → OrderShipped event

// Każdy service słucha events
// i emituje kolejne events

Pros: No central point of failure, loose coupling. Cons: Complex flow tracking, debugging harder.

Orchestration (Centralized)

Saga Orchestrator
  ↓ ProcessPayment command
Payment Service
  → PaymentProcessed response
  ↓
Orchestrator
  ↓ ReserveStock command
Inventory Service
  → StockReserved response
  ↓
Orchestrator
  ↓ ShipOrder command
Shipping Service

// Orchestrator kontroluje flow

Pros: Explicit flow, easier debugging. Cons: Central orchestrator dependency.

Saga Implementation - E-commerce Order

Order processing saga z compensating transactions:

public class OrderSagaOrchestrator
{
    private readonly IEventBus _eventBus;
    private readonly ISagaStateStore _stateStore;

    public async Task StartSaga(OrderPlacedEvent @event)
    {
        var sagaState = new OrderSagaState
        {
            SagaId = Guid.NewGuid(),
            OrderId = @event.OrderId,
            CurrentStep = SagaStep.Started,
            StartedAt = DateTime.UtcNow
        };

        await _stateStore.Save(sagaState);

        // Step 1: Process Payment
        await _eventBus.Send(new ProcessPaymentCommand
        {
            OrderId = @event.OrderId,
            Amount = @event.Total,
            SagaId = sagaState.SagaId
        });
    }

    // Payment successful
    public async Task Handle(PaymentProcessedEvent @event)
    {
        var saga = await _stateStore.Get(@event.SagaId);
        saga.CurrentStep = SagaStep.PaymentProcessed;
        saga.PaymentId = @event.PaymentId;
        await _stateStore.Update(saga);

        // Step 2: Reserve Stock
        await _eventBus.Send(new ReserveStockCommand
        {
            OrderId = saga.OrderId,
            SagaId = saga.SagaId
        });
    }

    // Stock reserved
    public async Task Handle(StockReservedEvent @event)
    {
        var saga = await _stateStore.Get(@event.SagaId);
        saga.CurrentStep = SagaStep.StockReserved;
        await _stateStore.Update(saga);

        // Step 3: Ship Order
        await _eventBus.Send(new ShipOrderCommand
        {
            OrderId = saga.OrderId,
            SagaId = saga.SagaId
        });
    }

    // Order shipped - saga complete
    public async Task Handle(OrderShippedEvent @event)
    {
        var saga = await _stateStore.Get(@event.SagaId);
        saga.CurrentStep = SagaStep.Completed;
        saga.CompletedAt = DateTime.UtcNow;
        await _stateStore.Update(saga);
    }

    // FAILURE HANDLING - Compensating Transactions

    // Payment failed
    public async Task Handle(PaymentFailedEvent @event)
    {
        var saga = await _stateStore.Get(@event.SagaId);
        saga.CurrentStep = SagaStep.Failed;
        saga.FailureReason = "Payment failed";
        await _stateStore.Update(saga);

        // Cancel order (no compensation needed - nothing done yet)
        await _eventBus.Publish(new OrderCancelledEvent
        {
            OrderId = saga.OrderId,
            Reason = "Payment failed"
        });
    }

    // Stock reservation failed
    public async Task Handle(StockReservationFailedEvent @event)
    {
        var saga = await _stateStore.Get(@event.SagaId);
        saga.CurrentStep = SagaStep.Compensating;
        await _stateStore.Update(saga);

        // Compensating: Refund payment
        await _eventBus.Send(new RefundPaymentCommand
        {
            PaymentId = saga.PaymentId,
            Amount = saga.OrderTotal,
            Reason = "Stock unavailable"
        });
    }

    // Shipping failed
    public async Task Handle(ShippingFailedEvent @event)
    {
        var saga = await _stateStore.Get(@event.SagaId);
        saga.CurrentStep = SagaStep.Compensating;
        await _stateStore.Update(saga);

        // Compensating Step 1: Release stock
        await _eventBus.Send(new ReleaseStockCommand
        {
            OrderId = saga.OrderId
        });

        // Compensating Step 2: Refund payment
        await _eventBus.Send(new RefundPaymentCommand
        {
            PaymentId = saga.PaymentId,
            Amount = saga.OrderTotal,
            Reason = "Shipping failed"
        });
    }
}

// Saga State
public class OrderSagaState
{
    public Guid SagaId { get; set; }
    public Guid OrderId { get; set; }
    public SagaStep CurrentStep { get; set; }
    public Guid? PaymentId { get; set; }
    public decimal OrderTotal { get; set; }
    public DateTime StartedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    public string? FailureReason { get; set; }
}

public enum SagaStep
{
    Started,
    PaymentProcessed,
    StockReserved,
    Completed,
    Compensating,
    Failed
}

Saga State Machine Visualization

                    ┌─────────────┐
                    │   Started   │
                    └──────┬──────┘
                           │
                    Process Payment
                           │
              ┌────────────┴────────────┐
              ▼                         ▼
    ┌──────────────────┐      ┌─────────────────┐
    │ Payment Success  │      │  Payment Failed │
    └────────┬─────────┘      └────────┬────────┘
             │                         │
      Reserve Stock              Cancel Order
             │                         │
    ┌────────┴─────────┐               ▼
    ▼                  ▼         ┌──────────┐
┌─────────┐    ┌──────────────┐ │  Failed  │
│Reserved │    │Reserve Failed│ └──────────┘
└────┬────┘    └──────┬───────┘
     │                │
Ship Order      Refund Payment
     │                │
┌────┴─────┐          ▼
▼          ▼    ┌──────────┐
┌────┐  ┌───────────┐│ Failed  │
│Done│  │Ship Failed│└──────────┘
└────┘  └─────┬─────┘
              │
      Release + Refund
              │
              ▼
        ┌──────────┐
        │  Failed  │
        └──────────┘

Najlepsze praktyki Saga Pattern

Idempotencja jest kluczowa: Każdy krok musi być idempotentny (retry-safe) – wielokrotne wykonanie musi dać ten sam rezultat.

Timeout handling: Zaimplementuj timeouty dla każdego kroku, aby wykrywać zawieszone operacje.

Persystencja stanu Saga: Przechowuj stan sagi w trwałym storage – musi przetrwać restart serwisu.

Compensating transactions: Transakcje kompensujące również muszą być idempotentne i niezawodne.

Monitoring i alerty: Śledź postęp sag, ustawiaj alerty dla zawieszonych sag. Używaj Saga pattern zamiast distributed 2PC (two-phase commit) w mikrousługach – 2PC po prostu nie działa w rozproszonych systemach.

Prawdziwy Case Study: Platforma E-commerce obsługująca 100k zamówień dziennie

Teoria to jedno, ale jak wygląda to w praktyce? Oto case study prawdziwej platformy e-commerce, która przetwarza ponad 100,000 zamówień dziennie.

System wykorzystuje Event Sourcing do audytu, CQRS do szybkich odczytów, Kafka jako event bus, i Saga pattern do zarządzania rozproszonymi transakcjami. Zobaczmy jak to wszystko współpracuje w praktyce:

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                        API Gateway                          │
└───────────────────────┬─────────────────────────────────────┘
                        │
        ┌───────────────┼───────────────┐
        ▼               ▼               ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│Order Service │ │Product Svc   │ │Customer Svc  │
│              │ │              │ │              │
│ Event Store  │ │ Event Store  │ │ Event Store  │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
       │                │                │
       └────────────────┼────────────────┘
                        │
                        ▼
                ┌──────────────┐
                │     Kafka    │
                │ Event Stream │
                └───────┬──────┘
                        │
        ┌───────────────┼───────────────────┐
        ▼               ▼                   ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│Payment Svc   │ │Inventory Svc │ │Shipping Svc  │
└──────────────┘ └──────────────┘ └──────────────┘
        │               │                   │
        └───────────────┼───────────────────┘
                        │
                        ▼
                ┌──────────────┐
                │ Projections  │
                │              │
                │ - SQL Read   │
                │ - Redis Cache│
                │ - Elastic    │
                └──────────────┘

Event Flow - Order Placement

1. Command: PlaceOrder

User → API Gateway → Order Service (Command Handler)

2. Event: OrderPlaced

Order Service → Event Store → Kafka (orders topic)

3. Saga Orchestrator: ProcessOrderSaga

Konsumuje OrderPlaced, koordynuje distributed transaction

4. Command: ProcessPayment

Saga → Payment Service

5. Event: PaymentProcessed

Payment Service → Kafka

6. Command: ReserveStock

Saga → Inventory Service

7. Event: StockReserved

Inventory Service → Kafka

8. Command: ShipOrder

Saga → Shipping Service

9. Event: OrderShipped

Shipping Service → Kafka → Multiple Consumers (Notification, Analytics, etc.)

10. Projections Update

OrderSummaryProjection konsumuje events, buduje read models

Technology Stack

Backend:

  • • .NET 8 (Microservices)
  • • Event Store (Marten / EventStoreDB)
  • • Apache Kafka (Event Streaming)
  • • MassTransit (Saga Orchestration)
  • • PostgreSQL (Event Store, Projections)
  • • Redis (Cache, Read Models)

Infrastructure:

  • • Azure Kubernetes Service (AKS)
  • • Azure Container Registry
  • • Confluent Cloud / Azure Event Hubs
  • • OpenTelemetry (Distributed Tracing)
  • • Grafana / Prometheus (Monitoring)
  • • Elasticsearch (Search Projections)

Results & Benefits

99.9%

Availability

Isolated failures - payment service down nie blokuje catalog browsing

10x

Scalability

Independent service scaling - inventory skaluje się niezależnie od orders

100%

Audit Trail

Event Sourcing zapewnia complete history dla compliance i debugging

50ms

Query Performance

CQRS read models zoptymalizowane - Redis cache dla hot data

Najczęściej zadawane pytania o Event-Driven Architecture

Czym jest Event-Driven Architecture i kiedy ją stosować?

Event-Driven Architecture (EDA) to wzorzec architektoniczny, w którym komponenty komunikują się poprzez eventy zamiast bezpośrednich wywołań synchronicznych. Producenci emitują eventy reprezentujące fakty ("zamówienie złożone"), a konsumenci niezależnie reagują na te eventy.

Stosuj EDA gdy potrzebujesz: luźnego powiązania między serwisami (loose coupling), przetwarzania asynchronicznego, możliwości odtwarzania eventów (event replay), skalowalności dla scenariuszy high-throughput. Idealne zastosowania: e-commerce (order processing), IoT (sensor data), systemy tradingowe, real-time analytics, notyfikacje użytkowników.

Jaka jest różnica między Event Sourcing a CQRS?

To dwa różne wzorce, które często są używane razem. Event Sourcing to sposób przechowywania danych – zamiast current state zapisujesz pełną sekwencję eventów (wszystkie zmiany). CQRS to wzorzec rozdzielający model zapisu od modelu odczytu.

Możesz używać ich niezależnie (CQRS bez Event Sourcing lub odwrotnie), ale razem tworzą potężną kombinację: Event Sourcing dla write side zapewnia pełny audit trail i możliwość replay, CQRS dla read side umożliwia zdenormalizowane, zoptymalizowane projekcje. W praktyce Event Sourcing naturalnie prowadzi do CQRS, bo masz event store (write) i projekcje (read).

Kafka vs RabbitMQ vs Azure Service Bus - który wybrać?

Każdy z tych message brokerów ma swoje mocne strony:

Apache Kafka świetnie sprawdza się przy event streaming i bardzo wysokim throughput (miliony msg/sec). Kluczowa różnica: Kafka przechowuje eventy długoterminowo i umożliwia ich odtwarzanie (replay). Idealny dla Event Sourcing, analytics pipelines, IoT sensor data.

RabbitMQ to tradycyjna message queue z elastycznym routingiem. Messages są transient (consumed = deleted). Lepszy dla task queues, RPC patterns, background jobs. Throughput niższy (do ~100k msg/sec), ale setup prostszy.

Azure Service Bus to w pełni zarządzany serwis w ekosystemie Azure. Zero operacji infrastrukturalnych, enterprise features (sessions, dead-letter queues, duplicate detection). Naturalny wybór dla Azure-native applications i gdy preferujesz managed service.

Jak działa Saga pattern w transakcjach rozproszonych?

W mikrousługach nie możesz użyć tradycyjnych ACID transactions rozpiętych na wiele serwisów. Saga pattern rozwiązuje ten problem implementując distributed transaction jako sekwencję lokalnych transakcji.

Każdy krok wykonuje lokalną transakcję i publikuje event po zakończeniu. Następny serwis słucha eventu i wykonuje swój krok. Gdy wszystko działa, saga kończy się sukcesem. Ale co gdy któryś krok się nie powiedzie?

Wtedy uruchamiają się compensating transactions – specjalne operacje cofające poprzednie kroki (np. zwrot płatności, zwolnienie zarezerwowanego stock). Masz dwa style: Choreography (zdecentralizowane, serwisy reagują na eventy) i Orchestration (centralny koordynator zarządza flow). Saga to sprawdzone rozwiązanie – używaj go zamiast distributed 2PC (two-phase commit), który nie działa w praktyce w systemach rozproszonych.

Jakie są główne wyzwania Event-Driven Architecture?

EDA nie jest srebrną kulą – przynosi też wyzwania. Najważniejsze:

Eventual consistency: Dane nie są natychmiast spójne między serwisami. Musisz zaakceptować, że przez krótki czas różne części systemu mogą widzieć różne stany. To wymiana za skalowalność.

Debugging complexity: W systemach rozproszonych request przepływa przez wiele serwisów. Potrzebujesz distributed tracing (np. OpenTelemetry) żeby śledzić flow.

Message ordering: W distributed systems trudno zagwarantować globalną kolejność. Rozwiązanie: partycjonowanie (events z tym samym kluczem trafiają do tej samej partycji).

Duplicate messages: Network issues mogą spowodować duplikaty. Każdy handler musi być idempotentny (wielokrotne wykonanie daje ten sam efekt).

Rozwiązania: Distributed tracing (OpenTelemetry, Jaeger), schema registry dla event schema evolution, idempotent handlers, monitoring i alerting dla stuck messages, automated testing (contract tests, chaos engineering).

Podsumowanie - Czy Event-Driven Architecture jest dla Ciebie?

Event-Driven Architecture to nie modny buzzword, ale sprawdzone rozwiązanie dla systemów, które muszą skalować się pod obciążeniem i przetrwać awarie.

Poznałeś już kluczowe wzorce: Event Sourcing daje pełną historię zmian, CQRS optymalizuje odczyty i zapisy osobno, Saga pattern zarządza rozproszonymi transakcjami, a message brokery jak Kafka, RabbitMQ i Azure Service Bus zapewniają niezawodne dostarczanie eventów.

Firmy implementujące EDA osiągają lepszą izolację błędów (awaria jednego serwisu nie zabija całego systemu), niezależne skalowanie serwisów, kompletną historię eventów dla audytu i compliance, oraz elastyczną ewolucję systemu bez przestojów. To inwestycja w długoterminową utrzymywalność systemu.

Jeśli budujesz system, który musi przetworzyć więcej niż kilkaset requestów na sekundę lub potrzebujesz pełnego audytu operacji, EDA przestaje być opcją a staje się koniecznością. Zobacz też nasze artykuły o architekturze mikrousług i rozwiązaniach chmurowych dla pełnego obrazu nowoczesnych architektur.

Potrzebujesz pomocy z implementacją Event-Driven Architecture?

Specjalizuję się w projektowaniu i implementacji production-ready systemów event-driven. Mam doświadczenie w Event Sourcing, CQRS, Kafka, architekturze mikrousług i transakcjach rozproszonych. Pomogę Ci zbudować skalowalny i odporny na awarie system, który będzie rozwijał się wraz z Twoim biznesem.

Powiązane artykuły

Event-Driven Architecture Patterns 2025 - Kompleksowy przewodnik | Wojciechowski.app