1. Event Schema Evolution
You ship events v1:
{ "type": "UserCreated", "userId": "123", "email": "alice@example.com" }
Six months later, you need to add a phone number field. You can't retroactively add it to old events without replaying history or backfilling.
Fix: Version your events.
{
"type": "UserCreated",
"version": 2,
"userId": "123",
"email": "alice@example.com",
"phoneNumber": "+1234567890"
}
Have a migration layer:
function migrateEvent(raw: any): Event {
if (raw.version === 1) {
return {
...raw,
version: 2,
phoneNumber: null
};
}
return raw;
}
2. Snapshot Performance Cliff
As your event stream grows to 100k events, replaying from genesis takes minutes. You add snapshots.
But if snapshots break, recovery is slow. And testing is a nightmare — you need old snapshot files to verify behavior.
Fix: Snapshot strategy from day one. Snapshot every N events or every M minutes, not as an afterthought.
const SNAPSHOT_INTERVAL = 1000; // events
async function loadAggregate(userId: string) {
const snapshot = await getLatestSnapshot(userId);
const fromVersion = snapshot?.version ?? 0;
const events = await getEventsSince(userId, fromVersion);
return applyEvents(snapshot?.state ?? {}, events);
}
3. Eventual Consistency Headaches
After you emit an event, the read model isn't updated yet. A user refreshes the page and sees stale data.
Without careful UX, users report bugs that don't exist.
Fixes:
- Optimistic updates: Update the UI immediately, revert on server mismatch
- Request-response acknowledgment: Return the new state from the command handler, not the read model
- Polling: JavaScript polls for updates every few seconds
4. Distributed Transaction Gotcha
Your "PaymentProcessed" event is persisted, but the email notification fails. Now the event happened, but the side effect didn't.
This is the outbox pattern problem.
Fix: Use an outbox table. Emit events and side-effect records in one transaction.
BEGIN;
INSERT INTO events (aggregate_id, type, data) VALUES (...);
INSERT INTO outbox (event_id, action, target) VALUES (...);
COMMIT;
-- Separate service polls outbox, sends notifications, marks as sent
5. Event Deletion (Compliance)
A user requests deletion (GDPR). You can't retroactively remove events from a immutable log.
Fixes:
- Encryption: Store PII encrypted, delete the key
- Pseudonymization: Hash user identifiers, keep historical events
- Soft delete: Mark events as "deleted" and skip in replay
- Rebasing: For critical compliance, rebuild the stream without sensitive events (rare, expensive)
Most practical teams use encryption + key deletion. The event stays, but the data is unreadable.