Skip to Content
Core ConceptsEvent Sourcing

Event Sourcing

Synap SDK is built on event sourcing - a powerful architectural pattern that records every change as an immutable event.

What is Event Sourcing?

Instead of storing only the current state, event sourcing stores all changes that led to the current state.

###Traditional Approach (State-Based)

// Update task directly database.tasks.update({ id: '123' }, { status: 'done', completedAt: new Date() }) // Lost information: // - Who completed it? // - When exactly? // - What was it before?

Event Sourcing Approach

// Record the event await synap.entities.update(taskId, { metadata: { status: 'done' } }) // Creates immutable event: { eventType: 'entities.update.validated', data: { status: 'done' }, userId: 'user_123', timestamp: '2024-12-20T15:30:00Z', version: 5 } // Benefits: // ✅ Complete audit trail // ✅ Who, what, when recorded // ✅ Can replay to any point in time

How Synap SDK Uses Event Sourcing

Write Path (Mutations)

All mutations are event-sourced:

// 1. SDK publishes event await synap.entities.create({ ... }) // 2. Event logged to database events.log({ eventType: 'entities.create.requested', data: { ... } }) // 3. Inngest worker processes entitiesWorker.handle(event) // 4. Database updated database.entities.insert({ ... }) // 5. Validation event emitted events.log({ eventType: 'entities.create.validated', data: { ... } })

Event-Sourced Operations:

  • entities.create()
  • entities.update()
  • entities.delete()
  • relations.create()
  • relations.delete()

Read Path (Queries)

All queries read directly from the database for speed:

// Fast, direct database query const tasks = await synap.entities.list({ type: 'task' }) // No event processing needed database.entities.findMany({ type: 'task' })

Direct-Read Operations:

  • entities.get()
  • entities.list()
  • entities.search()
  • relations.get()
  • relations.getRelated()
  • events.getHistory()

Benefits

1. Complete Audit Trail

Every change is recorded forever:

// See full history const history = await synap.events.getHistory(entityId) console.log(`Entity modified ${history.length} times`) history.forEach(event => { console.log(`${event.createdAt}: ${event.eventType} by ${event.userId}`) })

Use Cases:

  • Compliance (GDPR, SOC2)
  • Security audits
  • User accountability
  • Debugging

2. Time Travel

Replay events to see past states:

// Get state at any point in time const eventsUntil = history.filter(e => e.createdAt <= new Date('2024-12-01') ) // Rebuild state let pastState = {} eventsUntil.forEach(event => { if (event.eventType includes 'create') pastState = event.data if (event.eventType.includes('update')) pastState = { ...pastState, ...event.data } })

Use Cases:

  • Undo/redo
  • Historical reporting
  • Data recovery
  • Root cause analysis

3. Reliability

Async processing with automatic retries:

// If worker fails, Inngest retries automatically entitiesWorker.config({ retries: 3 }) // Your application stays responsive // Events are processed in background

Benefits:

  • Resilient to failures
  • Never lose data
  • Eventual consistency
  • Scalable processing

4. Real-Time Updates

Events trigger webhooks and broadcasts:

// When event is validated: // 1. Webhook sent to configured URL // 2. SSE broadcast to connected clients // 3. Real-time UI updates // Your frontend stays in sync automatically

Event Lifecycle

1. Request Event

await synap.entities.create({ type: 'task', title: 'Review PR' }) // Creates: { eventType: 'entities.create.requested', aggregateId: 'task_123', data: { type: 'task', title: 'Review PR' }, version: 1 }

2. Processing

// Inngest worker receives event entitiesWorker.handle(event) // Validates data // Inserts into database // Handles errors

3. Validation Event

// On success: { eventType: 'entities.create.validated', aggregateId: 'task_123', data: { id: 'task_123', type: 'task', title: 'Review PR', createdAt: '2024-12-20T10:00:00Z' }, version: 1 }

4. Notification

// Broadcast via SSE broadcast({ type: 'entity.created', entityId: 'task_123' }) // Trigger webhooks webhook.send({ event: 'entities.create.validated', data: { ... } })

Trade-Offs

Advantages ✅

  • Complete history: Never lose data
  • Audit trail: Every change tracked
  • Debugging: Replay to find issues
  • Flexibility: Add features without migrations
  • Testability: Events are facts, easy to test

Considerations ⚠️

  • Eventual consistency: Writes are async (usually < 100ms)
  • Storage: Events accumulate over time
  • Complexity: More moving parts than CRUD

For Synap SDK:

  • ✅ Consistency delay is minimal (background workers)
  • ✅ Storage managed by Synap infrastructure
  • ✅ Complexity hidden behind simple API

##Common Patterns

Check Event Status

// Create entity const { entityId } = await synap.entities.create({ ... }) // Wait a moment for processing await new Promise(resolve => setTimeout(resolve, 100)) // Verify it exists const entity = await synap.entities.get(entityId)

Event-Driven Features

// Listen for validation events // (In real app, use webhooks or SSE) function onEntityCreated(event) { if (event.data.type === 'task') { // Auto-assign to team synap.relations.create( event.data.id, defaultAssignee, 'assigned_to' ) } }

Event Replay

// Rebuild aggregate state async function getEntityVersion(entityId: string, version: number) { const history = await synap.events.getHistory(entityId) const eventsToReplay = history.slice(0, version) return replayEvents(eventsToReplay) }

Best Practices

✅ Do’s

// Trust eventual consistency const { entityId } = await synap.entities.create({ ... }) // Entity will exist very soon (< 100ms typically) // Use events for debugging const history = await synap.events.getHistory(entityId) console.log('Event sequence:', history.map(e => e.eventType)) // Implement optimistic UI setTasks([...tasks, newTask]) // Update immediately await synap.entities.create(newTask) // Persist in background

❌ Don’ts

// Don't assume immediate consistency const { entityId } = await synap.entities.create({ ... }) const entity = await synap.entities.get(entityId) // Might fail! // Add small delay if needed await new Promise(r => setTimeout(r, 50)) const entity = await synap.entities.get(entityId) // Better // Don't try to modify events // Events are immutable - create new ones instead

Learn More

Last updated on