ADR-013: Event-Driven Architecture

Status: Accepted Owner: @bilal @deen Date: 2025-01-13

Context

This ADR presents a simpler alternative to full Event Sourcing (ADR-012). Both address the same problems: incomplete event coverage, no external publishing, AI context service needs, and automation triggers.

The Question

Event Sourcing (ADR-012) makes events the source of truth and rebuilds state from them. Event-Driven (this ADR) keeps Prisma as the source of truth but emits events after mutations succeed.

Decision

Event-Driven Architecture as a pragmatic middle ground — 80% of the benefits with 20% of the complexity.

  • Prisma remains the source of truth
  • Events emitted after mutations — for audit and external systems
  • Events stored in unified domain_events table — replaces issue-specific issue_events
  • Events published to subscribers — n8n, AI service, etc.

Why Not Full Event Sourcing?

FactorEvent SourcingEvent-Driven
ComplexityHighMedium
Migration effort3-4 weeks1-2 weeks
DebuggingEvent replayStandard DB queries
Eventual consistencyYesNo (sync by default)
Team familiarityLowHigh

Can always upgrade to Event Sourcing later if needed.

Architecture

Dashboard (Next.js)
  GraphQL Mutations → Prisma Write → Emit Event → Response
                          ↓              ↓
                     [Source DB]    [Events Table]
                                        ↓
                              EVENT DISPATCHER
                         ↙         ↓          ↘
                      n8n     AI Context    Future
                   Workflows   Service     Services

Unified Events Table

CREATE TABLE domain_events (
    id              UUID PRIMARY KEY,
    entity_type     TEXT NOT NULL,      -- 'Issue', 'Property', 'Tenant', etc.
    entity_id       UUID NOT NULL,
    event_type      TEXT NOT NULL,      -- 'IssueCreated', 'PropertyUpdated', etc.
    data            JSONB NOT NULL,
    organisation_id UUID NOT NULL,
    user_id         UUID,
    correlation_id  UUID,
    source          TEXT NOT NULL DEFAULT 'dashboard',
    published_at    TIMESTAMPTZ,
    publish_error   TEXT,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Event Emitter Service

Singleton EventEmitter class that stores events in DB and publishes to registered publishers (n8n webhook, AI context service) asynchronously. Retry job handles failed publishes.

Event Types

Covers all entities: Issue (created, updated, status changed, deleted, vendor assigned, note added), Property (CRUD), Tenant (CRUD, moved property), Document (CRUD, expiring), Attachment (CRUD), Conversation (started, message received, resolved).

Migration Strategy

  1. Phase 1 (Day 1-2): Add infrastructure (table, Prisma model, emitter service)
  2. Phase 2 (Day 3-4): Dual write — Issue mutations emit events, keep old table
  3. Phase 3 (Day 5-7): Extend to all entities
  4. Phase 4 (Day 8-10): Connect external systems (n8n, AI context)
  5. Phase 5 (Day 11-14): Migrate old data, drop issue_events, update UI

Consequences

Positive

  • Complete event coverage for all entities
  • External publishing for n8n and AI service
  • Unified event model
  • Simpler than CQRS, easy migration path
  • Full audit trail

Negative

  • Events not source of truth (derived from state)
  • Potential drift if emit fails (mitigated by retry)
  • Extra latency (~5-10ms per mutation)
  • Storage growth (needs retention policy)