Most apps store state like this:
user.name = "Alice"
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
}
Event-based model:
UserCreated
UserNameUpdated
UserDeleted
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)
}
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
}
}
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
}
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
}
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
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)