CQRS und Event Sourcing erklärt cover image

CQRS und Event Sourcing erklärt

By Julian Krause · Monday, September 15, 2025 · ~4 min read

CQRS und Event Sourcing werden häufig in einem Atemzug genannt, sind aber eigenständige Muster, die unterschiedliche Probleme lösen. Zu verstehen, wann und wie man jedes einzelne einsetzt, ist entscheidend, um unnötige Komplexität zu vermeiden.

CQRS: Lesen und Schreiben trennen

Command Query Responsibility Segregation bedeutet, unterschiedliche Modelle zum Lesen und Schreiben von Daten zu verwenden. In der einfachsten Form sind das lediglich getrennte Lese- und Schreib-DTOs. In der fortgeschritteneren Form haben Sie unter Umständen vollständig getrennte Datenbanken.

// Command side - rich domain model
public class PlaceOrderCommand : IRequest<Guid>
{
    public required string CustomerId { get; init; }
    public required List<OrderLineDto> Lines { get; init; }
}

public class PlaceOrderHandler : IRequestHandler<PlaceOrderCommand, Guid>
{
    private readonly IOrderRepository _repository;

    public async Task<Guid> Handle(PlaceOrderCommand cmd, CancellationToken ct)
    {
        var order = Order.Create(new CustomerId(cmd.CustomerId));
        foreach (var line in cmd.Lines)
            order.AddLine(new ProductId(line.ProductId), line.Quantity, line.UnitPrice);

        await _repository.AddAsync(order, ct);
        await _repository.SaveChangesAsync(ct);
        return order.Id;
    }
}

// Query side - thin read model, no domain logic
public class GetOrderSummaryQuery : IRequest<OrderSummaryDto?>
{
    public Guid OrderId { get; init; }
}

public class GetOrderSummaryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto?>
{
    private readonly IDbConnection _db;

    public async Task<OrderSummaryDto?> Handle(GetOrderSummaryQuery query, CancellationToken ct)
    {
        const string sql = """
            SELECT o.Id, o.Status, o.CreatedAt, COUNT(ol.Id) AS LineCount, SUM(ol.Total) AS Total
            FROM Orders o
            LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
            WHERE o.Id = @OrderId
            GROUP BY o.Id, o.Status, o.CreatedAt
            """;

        return await _db.QuerySingleOrDefaultAsync<OrderSummaryDto>(sql, new { query.OrderId });
    }
}

Die Query-Seite verwendet Dapper mit rohem SQL, da sie nicht den Overhead eines vollständigen ORMs benötigt. Das ist einer der Hauptvorteile von CQRS: Sie können jede Seite unabhängig optimieren.

Event Sourcing: Zustand als Events speichern

Event Sourcing verfolgt einen grundlegend anderen Ansatz für die Persistenz. Statt den aktuellen Zustand zu speichern, speichern Sie jede Zustandsänderung als unveränderliches Event.

public abstract class AggregateRoot
{
    private readonly List<IDomainEvent> _uncommittedEvents = new();
    public int Version { get; protected set; }

    protected void RaiseEvent(IDomainEvent @event)
    {
        Apply(@event);
        _uncommittedEvents.Add(@event);
    }

    protected abstract void Apply(IDomainEvent @event);

    public IReadOnlyList<IDomainEvent> GetUncommittedEvents() => _uncommittedEvents;
    public void ClearUncommittedEvents() => _uncommittedEvents.Clear();
}

public class ShoppingCart : AggregateRoot
{
    public Guid Id { get; private set; }
    private readonly Dictionary<string, int> _items = new();

    public void AddItem(string productId, int quantity)
    {
        RaiseEvent(new ItemAdded(Id, productId, quantity, DateTime.UtcNow));
    }

    public void RemoveItem(string productId)
    {
        if (!_items.ContainsKey(productId))
            throw new InvalidOperationException("Item not in cart.");

        RaiseEvent(new ItemRemoved(Id, productId, DateTime.UtcNow));
    }

    protected override void Apply(IDomainEvent @event)
    {
        switch (@event)
        {
            case ItemAdded e:
                _items[e.ProductId] = _items.GetValueOrDefault(e.ProductId) + e.Quantity;
                break;
            case ItemRemoved e:
                _items.Remove(e.ProductId);
                break;
        }
        Version++;
    }
}

Der Event Store

Events werden in einem Append-only-Store persistiert. Um den aktuellen Zustand zu rekonstruieren, spielen Sie alle Events eines Aggregats erneut ab.

public class EventStore : IEventStore
{
    private readonly IDbConnection _db;

    public async Task SaveEventsAsync(Guid aggregateId, IEnumerable<IDomainEvent> events, int expectedVersion)
    {
        foreach (var @event in events)
        {
            expectedVersion++;
            await _db.ExecuteAsync(
                "INSERT INTO EventStore (AggregateId, Version, EventType, Data, Timestamp) VALUES (@AggregateId, @Version, @EventType, @Data, @Timestamp)",
                new
                {
                    AggregateId = aggregateId,
                    Version = expectedVersion,
                    EventType = @event.GetType().Name,
                    Data = JsonSerializer.Serialize(@event, @event.GetType()),
                    Timestamp = DateTime.UtcNow
                });
        }
    }
}

Wann welches Muster einsetzen

CQRS allein ist nützlich, wenn sich Ihre Lese- und Schreibmuster deutlich unterscheiden. Die meisten Anwendungen haben weit mehr Lese- als Schreibzugriffe, und die Lesemodelle sehen oft ganz anders aus als die Schreibmodelle. Sie können CQRS auch ohne Event Sourcing einsetzen.

Event Sourcing ist wertvoll, wenn Sie eine vollständige Audit-Historie benötigen, vergangene Zustände rekonstruieren müssen oder Ihre Domäne von Natur aus ereignisgetrieben ist. Finanzsysteme, Logistik-Tracking und kollaborative Bearbeitung sind gute Kandidaten.

Beides zusammen ergibt Sinn, wenn Sie Event Sourcing benötigen und optimierte Lese-Projektionen aus dem Event-Stream aufbauen möchten. Die Events werden zur einzigen Quelle der Wahrheit, und Sie projizieren sie in leseoptimierte Sichten.

Die ehrlichen Kompromisse

Event Sourcing bringt erhebliche Komplexität mit sich. Schema-Evolution ist schwierig. Debugging erfordert das erneute Abspielen von Events. Projektionen können in Verzug geraten. Setzen Sie es nicht ein, außer die Vorteile überwiegen die Kosten eindeutig. Für die meisten Business-Anwendungen ist ein gut entworfenes relationales Modell mit CQRS mehr als ausreichend.


Comments (2)

Hannah Tuesday, March 10, 2026 5:08 PM

I have a question: does this also apply to older versions?

Ben Sunday, March 8, 2026 8:08 PM

I agree, great article!

Clara Monday, March 9, 2026 8:08 PM

Good question, I'd like to know too.

Anna Wednesday, March 11, 2026 5:08 PM

Thanks for sharing — very helpful.

Leave a Comment