Event Sourcing
ArkitekturPersistensmønster hvor state gemmes som en immutable sekvens af events i stedet for en mutabel række i en tabel — giver komplet historik og tidsrejse over data.
Beskrivelse
Event Sourcing vender den klassiske persistens-model på hovedet. I stedet for at gemme nuværende state i en række der løbende opdateres, gemmes hver eneste ændring som et event i en append-only log. Nuværende state udledes ved at afspille alle events fra begyndelsen — eller fra et snapshot. Tag en bankkonto som eksempel. Klassisk vil man have en accounts-tabel med en balance-kolonne der løbende opdateres. Med Event Sourcing gemmes i stedet AccountOpened, MoneyDeposited(500), MoneyWithdrawn(100), MoneyDeposited(200). Nuværende saldo (600) er en beregning fra event-strømmen — den er ikke gemt direkte. Fordelen er komplet audit-trail out of the box. Du kan svare på 'hvad var saldoen 3. marts kl 14:22?' ved at afspille events op til det tidspunkt. Du kan bygge nye read-modeller ved at gen-projicere hele historikken. Fejl kan analyseres ved at se præcis hvad der skete i hvilken rækkefølge — ikke bare den korrupte slutstate. Mønstret er tæt knyttet til Domain-Driven Design og bruges ofte sammen med CQRS. Populære event stores inkluderer EventStoreDB, Marten (oven på PostgreSQL) og Axon Server. Kafka bruges også ofte som event log, selvom det ikke er en dedikeret event store. Event Sourcing er ikke gratis. Skema-evolution kræver disciplin — du kan ikke bare ALTER en event der er i produktion. Query-mønstre skal designes op front. Og for simple CRUD-applikationer er kompleksitetsomkostningen sjældent værdien værd.
Problem
Klassisk state-baseret persistens taber information. Når en bruger opdaterer sin adresse, forsvinder den gamle adresse — medmindre du eksplicit har bygget audit-logging. Ved bugs er det svært at rekonstruere hvad der faktisk skete: du ser kun slutstaten, ikke stien dertil. Complianc-krav som GDPR-audit, finansiel sporbarhed eller forensics kræver ofte at man kan svare på 'hvem ændrede hvad og hvornår' — noget der er dyrt at tilføje bagefter.
Løsning
Gem hver ændring som et immutabelt event i en append-only log. Rekonstruér nuværende state ved at afspille events fra starten (eller fra et snapshot). Historikken bliver systemets kildesandhed, og state er blot en projection.
Eksempel
// Klassisk state-baseret model
class Account {
balance: number
deposit(amount) {
this.balance += amount // gammel værdi tabt
db.accounts.update(this.id, { balance: this.balance })
}
}
// -----------------------------------------------------------
// Event Sourced model
// -----------------------------------------------------------
// Events er immutable facts om hvad der er sket
class AccountOpened {
constructor(accountId, owner, timestamp) { ... }
}
class MoneyDeposited {
constructor(accountId, amount, timestamp) { ... }
}
class MoneyWithdrawn {
constructor(accountId, amount, timestamp) { ... }
}
class AccountAggregate {
private events: DomainEvent[] = []
private balance = 0
// Business-metoder producerer events, ændrer ikke state direkte
open(accountId, owner) {
this.apply(new AccountOpened(accountId, owner, new Date()))
}
deposit(amount) {
if (amount <= 0) throw new Error('Amount must be positive')
this.apply(new MoneyDeposited(this.id, amount, new Date()))
}
withdraw(amount) {
if (amount > this.balance) throw new Error('Insufficient funds')
this.apply(new MoneyWithdrawn(this.id, amount, new Date()))
}
// Apply anvender event på in-memory state og gemmer det
private apply(event: DomainEvent) {
this.when(event)
this.events.push(event)
}
// When er ren state-transition — samme funktion bruges ved replay
private when(event: DomainEvent) {
if (event instanceof AccountOpened) {
this.balance = 0
} else if (event instanceof MoneyDeposited) {
this.balance += event.amount
} else if (event instanceof MoneyWithdrawn) {
this.balance -= event.amount
}
}
// Genopbyg fra event log
static loadFrom(events: DomainEvent[]) {
const acc = new AccountAggregate()
for (const e of events) acc.when(e)
return acc
}
}
// Persistens = append events
class EventStore {
async append(streamId, events) {
await db.events.insert(events.map(e => ({
stream_id: streamId,
event_type: e.constructor.name,
data: JSON.stringify(e),
timestamp: e.timestamp,
version: this.nextVersion(streamId)
})))
}
async load(streamId): DomainEvent[] {
const rows = await db.query(
'SELECT * FROM events WHERE stream_id = $1 ORDER BY version',
[streamId]
)
return rows.map(row => this.deserialize(row))
}
}
// Tidsrejse — hvad var saldoen den 3. marts kl 14:22?
const events = await store.load(accountId)
const historical = events.filter(e => e.timestamp <= new Date('2026-03-03T14:22:00'))
const account = AccountAggregate.loadFrom(historical)
console.log('Saldo på tidspunktet:', account.balance)Fordele
- +Komplet audit-trail — hver ændring er persisteret som fakta
- +Tidsrejse — udled state på ethvert historisk tidspunkt
- +Nye read-modeller kan bygges bagudrettet fra eksisterende events
- +Debug ved at afspille hændelser i rækkefølge, ikke gætte fra slutstate
- +Naturlig fit til event-drevne integrationer og microservices
Udfordringer
- !Skema-evolution kræver versioning af events — ingen ALTER
- !Query-mønstre skal designes op front via projections
- !Snapshots er nødvendige for lange event-streams (performance)
- !Højere kompleksitet — kræver disciplin i domænemodellering
- !GDPR right-to-be-forgotten er svært — immutable logs kan ikke bare slettes
Anvendelsesområder
- -Finansielle systemer med regulatorisk audit-krav
- -E-handel hvor hele ordre-lifecyclen skal spores
- -Kollaborative værktøjer (Google Docs, Figma) hvor undo/redo er kritisk
- -IoT og telemetri hvor rå eventstream har værdi ud over blot state
- -Systemer der kombineres med CQRS for optimeret læsning
Eksempler fra den virkelige verden
- -Banker der skal kunne rekonstruere ethvert kontoflow for regulatorer
- -Version control-systemer som Git — commits er per definition events
- -Kollaborative dokumenter der afspiller ændringer for undo/history
- -Order management i logistik — hver status-ændring er et event
- -Analytics-platforme hvor rå events streames til data warehouse