Introduction

Many modern systems combine a relational SQL database (the system of record for many domain objects) with an event store (append-only log of domain events) to enable analytics, integrations, CQRS read models, and auditability. Doing both well requires a clear consistency model so application correctness, user experience, and operational procedures remain predictable.

This article explains how to design a robust consistency model for a hybrid architecture that uses both SQL and an event store. You will get:

Diagrams use block-style format for clarity.

Problem Statement

When you update a domain entity in SQL and also emit an event to an event store, you have two pieces of truth to keep in sync:

  1. The state in SQL tables (current values).

  2. The event stream of domain happenings (audit, integration feed).

Challenges

Before building, define the consistency promises your system must make. These promises guide design.

Consistency Models (Quick Primer)

For hybrid SQL + Event Store, most practical architectures use Eventual or Causal consistency with compensating patterns for strongly consistent flows where necessary.

Recommended High-Level Architecture

               ┌─────────────┐
               │  Client UI  │
               └─────┬───────┘
                     │
             ┌───────▼────────┐
             │  API / Service │
             └───────┬────────┘
                     │
  ┌──────────────────┴─────────────────┐
  │      Transactional Boundary        │
  │  (SQL + Outbox in same DB TX)     │
  └──────┬──────────────────┬──────────┘
         │                  │
         │                  │
   ┌─────▼────┐       ┌─────▼─────┐
   │ SQL Read │       │ Outbox    │
   │ Models   │       │ Table     │
   └──────────┘       └─────┬─────┘
                             │ (background poll/publish)
                             ▼
                        ┌───────────┐
                        │ Event Bus │
                        └────┬──────┘
                             │
                      ┌──────▼───────┐
                      │ Projections  │
                      └──────────────┘

Key principle: Make event emission atomic with the SQL change. The most reliable pattern to achieve this is the Transactional Outbox.

Pattern 1. Transactional Outbox (Recommended)

Idea

When code changes SQL state, also insert an event-row into an Outbox table within the same database transaction. A separate background publisher reads the outbox, publishes events to the event store/broker, and marks them sent.

Benefits

Outbox Table Example (SQL Server)

CREATE TABLE Outbox (
  OutboxId BIGINT IDENTITY PRIMARY KEY,
  AggregateId UNIQUEIDENTIFIER NOT NULL,
  EventType NVARCHAR(200) NOT NULL,
  Payload NVARCHAR(MAX) NOT NULL,
  CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
  Processed BIT NOT NULL DEFAULT 0,
  ProcessedAt DATETIME2 NULL,
  AttemptCount INT NOT NULL DEFAULT 0
);

CREATE INDEX IX_Outbox_Processed_CreatedAt ON Outbox(Processed, CreatedAt);

Write Flow

  1. Begin DB transaction.

  2. Update domain tables.

  3. Insert JSON event into Outbox.

  4. Commit transaction.

Publisher (Background Worker)

.NET Publisher Pseudocode

while (!ct.IsCancellationRequested) {
  var events = db.Query("SELECT TOP 100 * FROM Outbox WHERE Processed = 0 ORDER BY CreatedAt").ToList();
  foreach(var e in events) {
    try {
      await eventBus.PublishAsync(e.EventType, e.Payload, dedupeId: e.OutboxId);
      db.Execute("UPDATE Outbox SET Processed=1, ProcessedAt=SYSUTCDATETIME() WHERE OutboxId=@id", e.OutboxId);
    } catch (Exception ex) {
      db.Execute("UPDATE Outbox SET AttemptCount=AttemptCount+1 WHERE OutboxId=@id", e.OutboxId);
    }
  }
  await Task.Delay(pollInterval);
}

Guarantees & Caveats

Pattern 2. Event Sourcing (Full Event Store as Source of Truth)

Idea

Instead of keeping current state in SQL, store every state change as events (append-only). The SQL read models are projections derived from events.

Pros

Cons

When to Use

Use Event Sourcing when domain invariants are naturally expressed as events, or when audit/replay capabilities are core to the domain (financial systems, ledgers). Otherwise prefer hybrid: SQL primary, outbox to event store.

Pattern 3. Change Data Capture (CDC)

Idea

Use DB-level CDC to stream table-level changes to an event bus (Debezium, SQL Server CDC). This can remove the need for explicit outbox inserts.

Pros

Cons

Exactly-Once vs At-Least-Once Semantics

Design consumers to be idempotent: store ProcessedEventId in projection DB with unique constraint, use upserts/merges.

Read Models and Projection Strategies

Projections read events and update SQL read tables:

Projection Upsert Pattern (SQL)

MERGE INTO OrderProjection AS target
USING (VALUES(@OrderId, @Total, @Version)) AS src(OrderId, Total, Version)
ON target.OrderId = src.OrderId
WHEN MATCHED AND target.Version < src.Version THEN
  UPDATE SET Total = src.Total, Version = src.Version
WHEN NOT MATCHED THEN
  INSERT (OrderId, Total, Version) VALUES (src.OrderId, src.Total, src.Version);

Keep Version to ensure ordering and idempotence.

Ordering Guarantees

Sagas and Long-Running Consistency

For multi-step, multi-aggregate operations (booking + payment + notification), use a Saga or orchestration that:

Sagas embed eventual consistency logic with compensating actions, not immediate global transactions.

UI Patterns: Angular Handling of Eventual Consistency

Users expect immediate confirmation. But the system may be eventually consistent. Use UX patterns to make this reliable and understandable.

1. Optimistic UI + Pending State

2. Read-After-Write Strategy

3. Staleness Indicator

4. Explicit Refresh / Polling

Example Angular Flow (Pseudo)

// After saving:
this.api.post('/orders', order).subscribe(resp => {
  this.localState.update(resp); // immediate
  this.startCheckingProjection(order.id); // poll until projection shows change
});

.NET Implementation Example: Transactional Outbox + Publisher

DbContext Save Helper (EF Core)

public async Task SaveChangesWithEventAsync(DbContext ctx, DomainEvent evt) {
  using var tx = await ctx.Database.BeginTransactionAsync();
  // domain updates
  await ctx.SaveChangesAsync();

  // outbox
  ctx.Set<Outbox>().Add(new Outbox { AggregateId=evt.AggregateId, EventType=evt.Type, Payload=JsonSerializer.Serialize(evt) });
  await ctx.SaveChangesAsync();

  await tx.CommitAsync();
}

Publisher Worker (Hosted Service)

Use single-producer or distributed publisher with claim semantics: UPDATE TOP (N) SET Processing = 1 OUTPUT ... WHERE Processed = 0 AND Processing = 0 to claim batch atomically.

Testing Strategy

Monitoring And Observability

Track:

Alert thresholds:

Operational Playbook

  1. Deploy Outbox Publisher First (if migrating): enable publisher to read new outbox rows before switching producers.

  2. Backfill or Replay Projections: support replay to rebuild read models after code fix.

  3. Schema Migrations: when changing domain schema, publish transformation events or handle old versions in projection handlers.

  4. Compensations: design compensation workflows for irreversible external side effects (refunds, reversals).

  5. Data Recovery: snapshot read models before mass replays.

Trade-Offs And Decision Guide

Common Pitfalls And How To Avoid Them

Example: End-to-End Scenario

Use Case: User updates item quantity -> SQL stock updated -> event emitted -> inventory projection updates global availability -> downstream reservation service consumes event.

Flow

  1. API updates Stock row and inserts StockAdjusted event into Outbox in same DB TX.

  2. Commit returns success to client; UI shows immediate change with “Confirmed”.

  3. Publisher reads outbox, publishes StockAdjusted event to Kafka partitioned by ItemId.

  4. Projection worker consumes event, upserts InventoryProjection with correct version.

  5. Reservation service consumes the same event (separate consumer group) and adjusts holds.

If publisher fails, outbox retains event and monitoring alerts engineers.

Summary

Designing a consistency model for a hybrid SQL + event store architecture is about making realistic promises and implementing patterns that deliver them reliably: