Händelsestyrd Arkitektur (Event-Driven Architecture)

Introduktion

Händelsestyrd arkitektur (EDA) är ett designmönster där systemets komponenter kommunicerar genom att publicera och konsumera händelser (events). Istället för att direkt anropa varandra är komponenterna löst kopplade via en gemensam eventbuss eller meddelandekö.


Grundbegrepp

Vad är ett Event?

Ett event är ett faktum — något som har hänt i systemet. Det är oföränderligt och beskrivs alltid i dåtid.

{
  "eventId":   "evt-7f3a9b2c",
  "eventType": "order.placed",
  "occurredAt": "2025-03-15T14:32:00Z",
  "payload": {
    "orderId":    "ord-001",
    "customerId": "cust-42",
    "totalAmount": 1299.00,
    "currency":   "SEK"
  }
}

Aktörer

RollAnsvar
ProducerPublicerar ett event när något händer (t.ex. Order Service publicerar order.placed)
Event BrokerTar emot, lagrar och distribuerar events (Kafka, RabbitMQ, Azure Service Bus)
ConsumerPrenumererar på events och reagerar på dem (t.ex. Payment Service, Email Service)

Arkitekturmönster

Pub/Sub (Publish-Subscribe)

Producers och consumers känner inte till varandra — de kommunicerar bara via topiken.

Order Service ──────▶ Topic: "order.placed" ──────▶ Payment Service
                                              ├────▶ Inventory Service
                                              └────▶ Email Service

Event Sourcing

Istället för att bara spara nuläget sparas alla händelser som lett dit. State rekonstrueras genom att spela upp eventhistoriken.

Events: [OrderCreated] → [ItemAdded] → [ItemAdded] → [OrderConfirmed]

                                                     Nuläge = summan

CQRS — Command Query Responsibility Segregation

Separerar skrivoperationer (Commands) från läsoperationer (Queries) i olika modeller — ofta kombinerat med Event Sourcing.

Command: PlaceOrder ──▶ Write Model ──▶ Event Store ──▶ Read Model ──▶ Query: GetOrder

Exempel — E-handelssystem (Java / Kafka)

Producer: Order Service

@Service
public class OrderService {

    private final KafkaTemplate<String, Object> kafkaTemplate;
    private final OrderRepository repository;

    public Order placeOrder(CreateOrderRequest request) {
        // 1. Spara ordern
        Order order = new Order(request.getCustomerId(), request.getItems());
        repository.save(order);

        // 2. Publicera event — vet inte vilka som lyssnar
        OrderPlacedEvent event = new OrderPlacedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getTotalAmount(),
            Instant.now()
        );

        kafkaTemplate.send("order.placed", order.getId(), event);
        return order;
    }
}

Consumer: Payment Service

@Component
public class PaymentEventHandler {

    private final PaymentService paymentService;

    @KafkaListener(topics = "order.placed", groupId = "payment-service")
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // Reagerar på eventet — vet inte var det kom ifrån
        try {
            PaymentResult result = paymentService.processPayment(
                event.getOrderId(),
                event.getCustomerId(),
                event.getTotalAmount()
            );

            if (result.isSuccessful()) {
                // Publicerar ett nytt event
                kafkaTemplate.send("payment.processed", new PaymentProcessedEvent(
                    event.getOrderId(), result.getTransactionId()
                ));
            } else {
                kafkaTemplate.send("payment.failed", new PaymentFailedEvent(
                    event.getOrderId(), result.getFailureReason()
                ));
            }
        } catch (Exception e) {
            // Dead Letter Queue — felaktiga meddelanden hamnar här för granskning
            kafkaTemplate.send("payment.dlq", event);
        }
    }
}

Consumer: Email Service

@Component
public class EmailEventHandler {

    private final EmailService emailService;
    private final CustomerRepository customerRepository;

    @KafkaListener(topics = "payment.processed", groupId = "email-service")
    public void handlePaymentProcessed(PaymentProcessedEvent event) {
        Customer customer = customerRepository.findByOrderId(event.getOrderId());
        emailService.sendOrderConfirmation(customer.getEmail(), event.getOrderId());
    }

    @KafkaListener(topics = "payment.failed", groupId = "email-service")
    public void handlePaymentFailed(PaymentFailedEvent event) {
        Customer customer = customerRepository.findByOrderId(event.getOrderId());
        emailService.sendPaymentFailedNotification(customer.getEmail(), event.getReason());
    }
}

Händelsekedjan visualiserad

Kund klickar "Köp"


Order Service ──▶ order.placed

           ┌───────────┼────────────┐
           ▼           ▼            ▼
   Payment Service  Inventory  Email Service
         │          Service    (väntar...)

  payment.processed

    ┌────┘

Email Service ──▶ "Orderbekräftelse skickad"

Fördelar

  • Löst koppling — Producers och consumers är oberoende av varandra.
  • Skalbarhet — Consumers kan skalas horisontellt oberoende.
  • Resiliens — Om Email Service är nere, buffras events och levereras när den startar om.
  • Auditlog — Alla events kan sparas och granskas i efterhand.
  • Extensibilitet — Lägg till nya consumers utan att ändra producers.

Nackdelar

  • Eventual consistency — Systemet är inte alltid i ett konsekvent tillstånd.
  • Svår debugging — Asynkrona flöden är svårare att följa och felsöka.
  • Duplicerade events — Consumers måste vara idempotenta (tåla samma event flera gånger).
  • Infrastrukturkostnad — Kräver en event broker (Kafka, RabbitMQ) att driftsätta.

Av Victor Hernandez från Bytebase.se