DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Event Sourcing Lite (Audit Trails Without the Complexity)

Most apps store state like this:

user.name = "Alice"
Enter fullscreen mode Exit fullscreen mode

That works…

until you need:

  • audit trails
  • debugging production issues
  • undo beyond a single session
  • sync conflict investigation
  • regulatory compliance
  • “how did we get here?” answers

At that point, storing only the latest state becomes a liability.

This post shows how to implement Event Sourcing Lite in SwiftUI — a practical approach that gives you:

  • history tracking
  • reproducible state
  • better debugging
  • safer sync reconciliation
  • production-grade observability

Without the complexity of full event-sourced systems.


🧠 The Core Principle

State is a result of events — not a single snapshot.

If you only store the final state, you lose the story.


🧱 1. Snapshot vs Event-Based Storage

Traditional model:

User {
  id
  name
}
Enter fullscreen mode Exit fullscreen mode

Event-based model:

UserCreated
UserNameUpdated
UserDeleted
Enter fullscreen mode Exit fullscreen mode

Current state = replay of events.


🧬 2. What Is Event Sourcing Lite?

Full event sourcing replaces your database.

Event Sourcing Lite:

  • keeps your normal database
  • adds an append-only event log
  • records meaningful changes

You get traceability without rewriting your app.


📦 3. Define Domain Events

enum UserEvent: Codable {
    case created(id: UUID, name: String)
    case nameUpdated(id: UUID, newName: String)
    case deleted(id: UUID)
}
Enter fullscreen mode Exit fullscreen mode

Events represent facts, not commands.


🧱 4. Event Store (Append-Only)

final class EventStore {
    func append(_ event: UserEvent) {
        // persist to database
    }

    func loadEvents() -> [UserEvent] {
        // fetch events
    }
}
Enter fullscreen mode Exit fullscreen mode

Rules:

  • never update events
  • never delete events
  • append only

History must be immutable.


🔄 5. Rebuilding State from Events

func rebuildUserState(from events: [UserEvent]) -> User? {
    var user: User?

    for event in events {
        switch event {
        case let .created(id, name):
            user = User(id: id, name: name)
        case let .nameUpdated(_, newName):
            user?.name = newName
        case .deleted:
            user = nil
        }
    }

    return user
}
Enter fullscreen mode Exit fullscreen mode

This enables:

  • debugging
  • consistency checks
  • migration recovery

🔁 6. Why This Helps Sync & Conflict Resolution

With events:

  • you can replay changes
  • you can merge event streams
  • conflicts become visible

Instead of:

“The state is wrong and we don’t know why.”

You can answer:

“This event caused the divergence.”


🧪 7. Debugging Production Issues

When a bug report arrives:

“My data disappeared.”

With event logs you can:

  • inspect event timeline
  • identify faulty updates
  • reproduce the state locally

Without logs, you’re guessing.


🧱 8. Event Sourcing Lite + Idempotency

Events must be idempotent.

Attach operation IDs:

struct EventEnvelope<Event: Codable>: Codable {
    let id: UUID
    let event: Event
    let timestamp: Date
}
Enter fullscreen mode Exit fullscreen mode

Prevents duplicate event replay.


⚠️ 9. When NOT to Use Event Sourcing Lite

Avoid for:

  • trivial apps
  • ephemeral data
  • UI-only state
  • high-frequency telemetry

Use it for:

  • user data
  • financial operations
  • collaborative edits
  • audit requirements

⚠️ 10. Common Anti-Patterns

Avoid:

  • storing events without timestamps
  • mutating past events
  • logging UI noise instead of domain events
  • replaying events without idempotency
  • mixing commands with events

Events are facts — not intentions.


🧠 Mental Model

Think:

User Action
 → Domain Event
   → Append-Only Log
     → State Projection
       → UI
Enter fullscreen mode Exit fullscreen mode

Not:

“Just overwrite the record.”


🚀 Final Thoughts

Event Sourcing Lite gives you:

  • audit trails
  • reproducible bugs
  • safer migrations
  • clearer sync reconciliation
  • long-term data trust

This is the difference between:

  • guessing what happened
  • and knowing exactly why

Top comments (0)