0Pricing
Node.js Backend Development Bootcamp · Leçon

Instantanés, gestion des versions et évolution du schéma d’événements

Accélérez la réhydratation avec des instantanés et faites évoluer les schémas d’événements sans altérer les données historiques.

Instantanés, gestion des versions et évolution du schéma d’événements est une leçon Node.js Backend Development Bootcamp gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Node.js Backend Development Bootcamp, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Node.js Backend Development Bootcamp comprend 4 leçons au total.

Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.

Why Rehydration Gets Slow

In Event Sourcing, the current state of an aggregate is rebuilt by replaying every event from the start of its stream. This is called rehydration.

For a fresh bank account with 12 events, replaying them is instant. But a long-lived aggregate (an account open for 5 years, a shopping cart with thousands of edits) can accumulate tens of thousands of events. Loading and folding all of them on every command becomes the bottleneck.

  • I/O cost: reading thousands of rows from the event store
  • CPU cost: folding each event through the reducer
  • Latency: every write must first rehydrate before validating the next command

The standard fix is a snapshot: a periodic, serialized copy of the aggregate state so you only replay events after it.

Rehydration as a Fold

Before optimizing, make rehydration explicit. An aggregate's state is a left fold over its events: state = events.reduce(apply, initialState).

Here is a minimal account aggregate. The apply function is a pure reducer — given the previous state and one event, it returns the next state. Notice it never mutates; it returns new objects so it stays easy to reason about and test.

function apply(state, event) {
  switch (event.type) {
    case 'AccountOpened':
      return { ...state, balance: 0, version: state.version + 1 };
    case 'MoneyDeposited':
      return { ...state, balance: state.balance + event.amount, version: state.version + 1 };
    case 'MoneyWithdrawn':
      return { ...state, balance: state.balance - event.amount, version: state.version + 1 };
    default:
      return state;
  }
}

function rehydrate(events) {
  const initial = { balance: 0, version: 0 };
  return events.reduce(apply, initial);
}

const events = [
  { type: 'AccountOpened' },
  { type: 'MoneyDeposited', amount: 100 },
  { type: 'MoneyWithdrawn', amount: 30 },
];

console.log(rehydrate(events)); // { balance: 70, version: 3 }

What a Snapshot Actually Is

A snapshot is a saved tuple of (aggregateId, version, serializedState). The version is the event sequence number the state reflects — this is the critical part.

To rehydrate with a snapshot:

  • Load the latest snapshot for the aggregate
  • Seed the fold with the snapshot state instead of initialState
  • Replay only events with version > snapshot.version

Key rule: a snapshot is a derived cache, never the source of truth. The event stream remains authoritative. If a snapshot is lost or corrupted, you can always rebuild it by replaying from zero.

Rehydrating From a Snapshot

This version loads from a snapshot when one exists, then folds only the newer events. The snapshot already encodes version, so we know exactly where to resume.

In production the snapshot and the events come from your store; here they are passed in so the logic runs standalone.

function apply(state, event) {
  switch (event.type) {
    case 'MoneyDeposited':
      return { ...state, balance: state.balance + event.amount, version: event.version };
    case 'MoneyWithdrawn':
      return { ...state, balance: state.balance - event.amount, version: event.version };
    default:
      return { ...state, version: event.version };
  }
}

function loadFromSnapshot(snapshot, allEvents) {
  const start = snapshot ? snapshot.state : { balance: 0, version: 0 };
  const fromVersion = snapshot ? snapshot.version : 0;
  const newer = allEvents.filter((e) => e.version > fromVersion);
  return newer.reduce(apply, start);
}

const snapshot = { version: 3, state: { balance: 70, version: 3 } };
const events = [
  { type: 'MoneyDeposited', amount: 100, version: 1 },
  { type: 'MoneyWithdrawn', amount: 30, version: 3 },
  { type: 'MoneyDeposited', amount: 50, version: 4 },
];

console.log(loadFromSnapshot(snapshot, events)); // { balance: 120, version: 4 }

When to Take a Snapshot

You don't snapshot on every event — that defeats the purpose. Common strategies:

  • Every N events: e.g. snapshot whenever version % 100 === 0. Simple and predictable.
  • By replay cost: snapshot when the gap between current version and last snapshot exceeds a threshold.
  • Async / background: a separate worker subscribes to the stream and writes snapshots off the hot path, so writes never wait on snapshotting.

Tune N empirically. Too low wastes storage and write throughput; too high means long replays. For many systems N = 50–200 is a reasonable starting point.

const SNAPSHOT_EVERY = 100;

function shouldSnapshot(currentVersion, lastSnapshotVersion) {
  // Snapshot when we've crossed another multiple of N
  const since = currentVersion - lastSnapshotVersion;
  return since >= SNAPSHOT_EVERY;
}

console.log(shouldSnapshot(100, 0));   // true
console.log(shouldSnapshot(140, 100)); // false
console.log(shouldSnapshot(205, 100)); // true

The Hidden Trap: Snapshots Are Versioned Too

Here is the subtle danger. A snapshot stores the shape of your state at the moment it was written. If you later change how state is structured (rename a field, add a required property, change a type), old snapshots become incompatible.

Imagine you stored { balance: 70 }, then refactored to { balanceCents: 7000 }. A six-month-old snapshot still has balance, and your new apply code expects balanceCents. Rehydration silently produces wrong state.

Rule: tag every snapshot with a schemaVersion. On load, if the snapshot's schema version doesn't match the current code, discard it and replay from the events (which are upcasted — covered next). Events are forever; snapshots are disposable.

Guarding Snapshot Schema Version

This guard checks the snapshot's schemaVersion before trusting it. If it's stale, we fall back to a full replay. Because events are the source of truth, falling back is always safe — it's just slower for that one load, and the next snapshot will be written in the new shape.

const CURRENT_SCHEMA = 2;

function chooseStartState(snapshot) {
  if (snapshot && snapshot.schemaVersion === CURRENT_SCHEMA) {
    return { state: snapshot.state, fromVersion: snapshot.version };
  }
  // Stale or missing snapshot: replay everything
  return { state: { balanceCents: 0, version: 0 }, fromVersion: 0 };
}

const stale = { schemaVersion: 1, version: 300, state: { balance: 70 } };
const fresh = { schemaVersion: 2, version: 300, state: { balanceCents: 7000, version: 300 } };

console.log(chooseStartState(stale).fromVersion); // 0 -> full replay
console.log(chooseStartState(fresh).fromVersion); // 300 -> fast path

Evolving Event Schemas: The Real Constraint

Events are immutable historical facts. You can never go back and edit a MoneyDeposited event from two years ago — it really happened in that shape. Yet your code must keep evolving.

So when business needs change, you cannot rewrite history. Instead you have a small set of disciplined techniques:

  • Weak schema / additive change: only ever add optional fields; never remove or repurpose existing ones.
  • Upcasting: transform old event versions into the current shape at read time.
  • New event type: when a change is too big, introduce a new event type and stop emitting the old one.

The golden rule: old events must remain readable forever. Every consumer reads through an upcaster, so the rest of your code only ever sees the latest shape.

Upcasting Old Events at Read Time

Upcasting means: as each event is read from the store, run it through a function that migrates older versions up to the current shape before it reaches apply. Each event carries an eventVersion so the upcaster knows what to do.

Example evolution: v1 deposits had a single amount in dollars. v2 splits money into currency + integer amountCents. Old v1 events get upcasted on the fly — without ever touching the stored data.

function upcast(event) {
  if (event.type === 'MoneyDeposited' && (event.eventVersion || 1) === 1) {
    return {
      ...event,
      eventVersion: 2,
      currency: 'USD',
      amountCents: Math.round(event.amount * 100),
    };
  }
  return event;
}

const stored = [
  { type: 'MoneyDeposited', amount: 100 },                         // legacy v1
  { type: 'MoneyDeposited', eventVersion: 2, currency: 'USD', amountCents: 5000 },
];

const upcasted = stored.map(upcast);
console.log(upcasted[0]); // { ...amountCents: 10000, currency: 'USD', eventVersion: 2 }
console.log(upcasted.every((e) => typeof e.amountCents === 'number')); // true

Chaining Upcasters Across Multiple Versions

Schemas evolve more than once. The clean pattern is a chain of single-step upcasters: v1→v2, v2→v3, v3→v4. An event enters at its stored version and is pushed forward step by step until it reaches the latest version. This keeps each migration small, testable, and independent.

Never write a giant v1→v4 function — you'd duplicate logic and it rots fast. Compose small steps instead.

const upcasters = {
  1: (e) => ({ ...e, eventVersion: 2, amountCents: Math.round(e.amount * 100) }),
  2: (e) => ({ ...e, eventVersion: 3, currency: e.currency || 'USD' }),
  3: (e) => ({ ...e, eventVersion: 4, source: e.source || 'unknown' }),
};

const LATEST = 4;

function upcastToLatest(event) {
  let e = { ...event, eventVersion: event.eventVersion || 1 };
  while (e.eventVersion < LATEST) {
    e = upcasters[e.eventVersion](e);
  }
  return e;
}

const legacy = { type: 'MoneyDeposited', amount: 42 };
const result = upcastToLatest(legacy);
console.log(result.eventVersion);  // 4
console.log(result.amountCents);   // 4200
console.log(result.currency);      // USD
console.log(result.source);        // unknown

Putting It Together: Safe, Fast Loading

A production rehydration path combines everything:

  • Load the latest snapshot; discard it if its schemaVersion is stale
  • Read events after the (valid) snapshot version
  • Upcast every event to the latest shape
  • Fold them onto the start state
  • Optionally write a new snapshot if the replay gap was large

This gives you O(events-since-snapshot) loads in the happy path, automatic correctness when state shape changes, and forward-compatible event reading. Snapshots make it fast; upcasting keeps it correct as the system evolves.

Operational tip: when you ship a breaking state refactor, you don't need to migrate snapshots — just bump CURRENT_SCHEMA and let the next load rebuild them lazily.

Quick Check

Test your understanding of the safe-evolution strategy.

Recap

You learned how to make Event Sourcing both fast and durable over time:

  • Rehydration is a left fold over events; long streams make it slow.
  • Snapshots store (aggregateId, version, state) so you replay only events after them — a derived cache, never the source of truth.
  • Snapshot strategy: take one every N events or by replay-gap, ideally on a background worker.
  • Tag snapshots with schemaVersion; discard and full-replay when state shape changes — no snapshot migration needed.
  • Events are immutable. Evolve safely with additive-only changes, upcasting old events to the latest shape at read time, or introducing new event types.
  • Chain single-step upcasters (v1→v2→v3) so every consumer sees only the current shape.

Snapshots give you speed; versioning and upcasting give you the freedom to evolve without ever breaking history.

Questions Fréquemment Posées

La leçon « Instantanés, gestion des versions et évolution du schéma d’événements » est-elle gratuite ?

Oui — le texte complet de « Instantanés, gestion des versions et évolution du schéma d’événements » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Node.js Backend Development Bootcamp, passe à CoddyKit PRO. Le cours Node.js Backend Development Bootcamp comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Instantanés, gestion des versions et évolution du schéma d’événements » ?

Accélérez la réhydratation avec des instantanés et faites évoluer les schémas d’événements sans altérer les données historiques. Tu pratiques Node.js Backend Development Bootcamp avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Node.js Backend Development Bootcamp ?

Aucune expérience préalable n'est requise. Node.js Backend Development Bootcamp sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Instantanés, gestion des versions et évolution du schéma d’événements » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Node.js Backend Development Bootcamp ?

Oui. Chaque leçon Node.js Backend Development Bootcamp inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Les événements comme source de vérité et le journal en ajout uniquement
  2. Agrégats, commandes et modélisation des événements métier
  3. Créer des modèles de lecture et des projections
  4. Instantanés, gestion des versions et évolution du schéma d’événements
← Retour à Node.js Backend Development Bootcamp