@shirudo/ddd-kit 1.0.0-rc.8 → 1.0.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -52,6 +52,101 @@ interface IdGenerator<Tag extends string> {
52
52
  next: () => Id<Tag>;
53
53
  }
54
54
 
55
+ /**
56
+ * Abstract base for **domain-invariant violations**. Domain methods
57
+ * (aggregates, entity validation hooks, value-object constructors)
58
+ * throw `DomainError`-derived exceptions when a business rule is
59
+ * violated. Consumers derive their own concrete errors — e.g.
60
+ * `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
61
+ * for `instanceof`-style catching at the App-Service boundary, where
62
+ * they typically map to HTTP 400 / business-rule responses.
63
+ *
64
+ * The library itself does **not** ship any concrete `DomainError`
65
+ * subclass — the kit can't know your invariants.
66
+ *
67
+ * Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
68
+ * surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
69
+ * `isRetryable`, …).
70
+ */
71
+ declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
72
+ }
73
+ /**
74
+ * Abstract base for **infrastructure / persistence failures** that the
75
+ * App-Service can recover from — typically by retrying, by returning
76
+ * HTTP 404 / 409, or by surfacing a "please try again" UX. These are
77
+ * not domain-invariant violations (the business rules were not
78
+ * broken); they describe race conditions and missing rows at the
79
+ * storage boundary.
80
+ *
81
+ * Library-internal concrete subclasses: {@link AggregateNotFoundError},
82
+ * {@link ConcurrencyConflictError}.
83
+ */
84
+ declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
85
+ }
86
+ /**
87
+ * Thrown by `EventSourcedAggregate.apply()` when no handler is
88
+ * registered for the event's type. This means the aggregate's subclass
89
+ * forgot to add an entry to its `handlers` map — a programming /
90
+ * configuration bug, not a domain or infrastructure failure.
91
+ *
92
+ * Deliberately **not** on `DomainError` or `InfrastructureError` —
93
+ * a generic `catch (e instanceof DomainError)` handler at the App
94
+ * layer must not mask a forgotten handler; this should crash loud and
95
+ * fail the calling Use Case so the bug surfaces in development. The
96
+ * replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
97
+ * also let it propagate uncaught instead of wrapping it in `Result.Err`.
98
+ *
99
+ * Use `isBaseError(e)` from `@shirudo/base-error` to detect
100
+ * "any structured error from the kit or any other BaseError-using
101
+ * library" at the App boundary.
102
+ */
103
+ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
104
+ readonly eventType: string;
105
+ constructor(eventType: string, cause?: unknown);
106
+ }
107
+ /**
108
+ * Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
109
+ * given id does not exist. `InfrastructureError` because the storage
110
+ * boundary, not a business rule, decided the row is absent. Use the
111
+ * nullable variant `getById()` if "not found" is a valid outcome.
112
+ *
113
+ * Accepts an optional `cause` so a `Repository.save()` implementation
114
+ * can wrap a lower-level "row not found" / driver-level error without
115
+ * losing context. Cause-chain helpers (`getRootCause`,
116
+ * `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
117
+ *
118
+ * Not retryable — retrying won't make the row appear.
119
+ */
120
+ declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
121
+ readonly aggregateType: string;
122
+ readonly id: string;
123
+ constructor(aggregateType: string, id: string, cause?: unknown);
124
+ }
125
+ /**
126
+ * Thrown by `IRepository.save()` when the aggregate's expected version
127
+ * does not match the version currently persisted — i.e. another writer
128
+ * updated the aggregate concurrently. The canonical optimistic-
129
+ * concurrency signal; the App-Service typically reloads, re-applies
130
+ * the use case, and retries, or surfaces HTTP 409 to the caller.
131
+ *
132
+ * `InfrastructureError` because the persistence layer (not a domain
133
+ * rule) detects the race. Marks itself as `retryable: true` so the
134
+ * `isRetryable` predicate from `@shirudo/base-error` picks it up.
135
+ */
136
+ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
137
+ readonly aggregateType: string;
138
+ readonly aggregateId: string;
139
+ readonly expectedVersion: number;
140
+ readonly actualVersion: number;
141
+ /**
142
+ * Marks this error as retryable so `isRetryable(err)` returns
143
+ * true. The canonical OCC pattern is to reload the aggregate, re-apply
144
+ * the use case, and retry on this exception.
145
+ */
146
+ readonly retryable: true;
147
+ constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
148
+ }
149
+
55
150
  /**
56
151
  * Factory function producing a fresh, unique event identifier for each call.
57
152
  *
@@ -385,6 +480,93 @@ declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?:
385
480
  */
386
481
  declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
387
482
 
483
+ type Version = number & {
484
+ readonly __v: true;
485
+ };
486
+ /**
487
+ * Snapshot of an aggregate state at a specific point in time.
488
+ * Used for optimizing event replay by starting from a snapshot
489
+ * instead of replaying all events from the beginning.
490
+ *
491
+ * @template TState - The type of the aggregate state
492
+ */
493
+ interface AggregateSnapshot<TState> {
494
+ /**
495
+ * The state of the aggregate at the time of the snapshot.
496
+ */
497
+ state: TState;
498
+ /**
499
+ * The version of the aggregate when the snapshot was taken.
500
+ */
501
+ version: Version;
502
+ /**
503
+ * Timestamp when the snapshot was created.
504
+ */
505
+ snapshotAt: Date;
506
+ }
507
+ /**
508
+ * Public contract every Aggregate Root satisfies. Implemented by
509
+ * `BaseAggregate` and inherited by both `AggregateRoot` and
510
+ * `EventSourcedAggregate`. Repository implementations type their
511
+ * `save(aggregate)` parameter against this interface rather than the
512
+ * concrete classes, so the repo layer does not take a compile-time
513
+ * dependency on the aggregate hierarchy.
514
+ *
515
+ * Full per-member documentation lives on the concrete `BaseAggregate`
516
+ * class; the interface is intentionally terse to avoid drift.
517
+ *
518
+ * @template TId - The aggregate root identifier (branded via `Id<Tag>`)
519
+ * @template TEvent - The domain-event union, defaults to `never`
520
+ */
521
+ interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
522
+ readonly id: TId;
523
+ readonly version: Version;
524
+ readonly persistedVersion: Version | undefined;
525
+ readonly pendingEvents: ReadonlyArray<TEvent>;
526
+ clearPendingEvents(): void;
527
+ markPersisted(version: Version): void;
528
+ }
529
+ /**
530
+ * Public contract for Event-Sourced Aggregate Roots. Extends
531
+ * `IAggregateRoot` with the replay-from-history boundary.
532
+ *
533
+ * @template TId - The aggregate root identifier
534
+ * @template TEvent - The union type of all domain events
535
+ */
536
+ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
537
+ /**
538
+ * Reconstitutes the aggregate from an event history. Returns
539
+ * `Result` because event-stream corruption is an expected
540
+ * recoverable failure at the infrastructure boundary.
541
+ */
542
+ loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
543
+ }
544
+ /**
545
+ * Checks if two aggregates are at the same version (same ID and version).
546
+ * Useful for optimistic concurrency control checks.
547
+ *
548
+ * Note: Two aggregates with the same ID ARE the same aggregate (identity).
549
+ * This function checks if they are at the same version — i.e., no concurrent modification.
550
+ *
551
+ * @example
552
+ * ```typescript
553
+ * const before = await repository.getById(id);
554
+ * // ... some operations ...
555
+ * const after = await repository.getById(id);
556
+ *
557
+ * if (!sameVersion(before, after)) {
558
+ * throw new Error("Aggregate was modified by another process");
559
+ * }
560
+ * ```
561
+ */
562
+ declare function sameVersion<TId extends Id<string>>(a: {
563
+ id: TId;
564
+ version: Version;
565
+ }, b: {
566
+ id: TId;
567
+ version: Version;
568
+ }): boolean;
569
+
388
570
  /**
389
571
  * Entity utilities and interfaces for Domain-Driven Design.
390
572
  *
@@ -715,126 +897,28 @@ declare function replaceEntityById<TId extends Id<string>, T extends Identifiabl
715
897
  declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
716
898
 
717
899
  /**
718
- * Marker interface for Aggregate Roots.
719
- *
720
- * In Domain-Driven Design, an Aggregate Root is an Entity (the parent Entity of the aggregate).
721
- * It represents the aggregate externally and is the only object that external code
722
- * is allowed to hold references to. All access to child entities within the aggregate
723
- * must go through the Aggregate Root.
724
- *
725
- * An Aggregate consists of:
726
- * - One Aggregate Root (Entity with id + version)
727
- * - Optional child entities (Entities with id + state, but no own version)
728
- * - Optional value objects
729
- *
730
- * The Aggregate Root has identity (id), state, and version for optimistic concurrency control.
731
- * Child entities exist only within the aggregate boundary and are versioned through
732
- * the Aggregate Root.
733
- *
734
- * @template TId - The type of the aggregate root identifier
735
- *
736
- * @example
737
- * ```typescript
738
- * class Order extends AggregateRoot<OrderState, OrderId> implements IAggregateRoot<OrderId> {
739
- * // Order is an Aggregate Root (an Entity with version)
740
- * // OrderState contains child entities (e.g., OrderItem) and value objects
741
- * }
742
- * ```
743
- */
744
- interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
745
- /**
746
- * Unique identifier of the aggregate root entity.
747
- */
748
- readonly id: TId;
749
- /**
750
- * Version number for optimistic concurrency control.
751
- * Incremented on each state change to detect concurrent modifications.
752
- * This version applies to the entire aggregate, including all child entities.
753
- */
754
- readonly version: Version;
755
- /**
756
- * Read-only list of domain events recorded on this aggregate that have
757
- * not yet been flushed to the outbox / persistence layer. Both state-
758
- * stored (`AggregateRoot`) and event-sourced (`EventSourcedAggregate`)
759
- * aggregates expose them under the same name, so Repository.save() can
760
- * harvest them uniformly without branching on the aggregate flavour.
761
- */
762
- readonly pendingEvents: ReadonlyArray<TEvent>;
763
- /**
764
- * Clears the pending-event list. Called by `markPersisted` after a
765
- * successful write — the events have been handed off to the outbox
766
- * / event store and are no longer the aggregate's responsibility.
767
- */
768
- clearPendingEvents(): void;
769
- /**
770
- * Post-save hook: a `Repository.save()` implementation calls this with
771
- * the persisted version after a successful write to push the new
772
- * version back into the aggregate and clear pendingEvents (they are
773
- * now safely on the write side / in the outbox).
774
- *
775
- * Required by the interface so a Repository implementation can call it
776
- * via the published `IAggregateRoot` contract without taking the
777
- * abstract class as a compile-time dependency.
778
- *
779
- * @param version - The version assigned by the persistence layer
780
- */
781
- markPersisted(version: Version): void;
782
- }
783
- /**
784
- * Configuration options for AggregateRoot behavior.
785
- */
786
- interface AggregateConfig {
787
- /**
788
- * Whether `setState()` should bump the version automatically when the
789
- * caller omits the per-call `bumpVersion` argument.
790
- *
791
- * Defaults to **`false`** — `setState()` already takes an explicit
792
- * `bumpVersion` argument per call, so the config is just the default
793
- * the per-call argument falls back to. Set to `true` only if you have
794
- * a subclass that never passes `bumpVersion` and you want every state
795
- * change to advance the version anyway.
796
- */
797
- autoVersionBump?: boolean;
798
- }
799
- /**
800
- * Base class for Aggregate Roots without Event Sourcing.
801
- *
802
- * In DDD (Evans), an Aggregate is a cluster of objects — root entity, child entities,
803
- * and value objects — treated as a unit for consistency. The **Aggregate Root** is the
804
- * root entity that represents the aggregate externally and is the only entry point
805
- * for external code. This class serves as both: it IS the root entity and it contains
806
- * the aggregate state (`TState`) which holds child entities and value objects.
807
- *
808
- * Provides:
809
- * - Identity (id) and state management (via `Entity`)
810
- * - Version management for optimistic concurrency control
811
- * - Domain event tracking for side-effects
812
- * - Snapshot support for performance optimization
813
- *
814
- * All changes to child entities within `TState` are versioned through this root.
815
- * Use `setState()` for state mutations to ensure invariant validation.
816
- *
817
- * For event sourcing, use `EventSourcedAggregate` instead.
818
- *
819
- * @template TState - The type of the aggregate state (contains child entities and value objects)
820
- * @template TId - The type of the aggregate root identifier
821
- * @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never` — aggregates without a declared event type cannot emit events (emitting any event becomes a compile error). Supply a concrete event union to opt in.
822
- *
823
- * @example
824
- * ```typescript
825
- * // Order is an Aggregate Root (an Entity with version)
826
- * class Order extends AggregateRoot<OrderState, OrderId> {
827
- * constructor(id: OrderId, initialState: OrderState) {
828
- * super(id, initialState);
829
- * }
900
+ * Shared base for both `AggregateRoot` (state-stored) and
901
+ * `EventSourcedAggregate`. Carries the lifecycle machinery that's
902
+ * identical across the two flavours: version + persistedVersion
903
+ * tracking, pending events buffer, the `markRestored` (Post-Load) /
904
+ * `markPersisted` (Post-Save) lifecycle markers, and the
905
+ * `recordEvent` helper that auto-injects `aggregateId` +
906
+ * `aggregateType` on every event the aggregate emits.
907
+ *
908
+ * Consumers do NOT extend this class directly extend
909
+ * `AggregateRoot` for state-stored aggregates or
910
+ * `EventSourcedAggregate` for event-sourced ones. The split between
911
+ * those two reflects the canonical Vernon §8 (state-stored) /
912
+ * Vernon §11 + Greg Young (event-sourced) distinction in how state
913
+ * is represented; the lifecycle machinery is the same for both.
830
914
  *
831
- * confirm(): void {
832
- * this.setState({ ...this.state, status: "confirmed" }, true);
833
- * }
834
- * }
835
- * ```
915
+ * @template TState - The type of the aggregate state
916
+ * @template TId - The aggregate root identifier
917
+ * @template TEvent - The domain-event union. Defaults to `never` so
918
+ * aggregates without a declared event type cannot emit events
919
+ * (emitting any event becomes a compile error).
836
920
  */
837
- declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
921
+ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
838
922
  /**
839
923
  * The aggregate's domain type as a string, used to populate
840
924
  * `aggregateType` on events recorded via {@link recordEvent}.
@@ -857,421 +941,62 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
857
941
  */
858
942
  protected abstract readonly aggregateType: string;
859
943
  private _version;
860
- get version(): Version;
861
- protected setVersion(version: Version): void;
862
- private readonly _config;
863
- private readonly _autoVersionBump;
864
- private _pendingEvents;
865
- /**
866
- * Read-only list of domain events recorded on this aggregate that have
867
- * not yet been flushed to the outbox / persistence layer.
868
- */
869
- get pendingEvents(): ReadonlyArray<TEvent>;
870
944
  /**
871
- * Clears the pending-event list. Call this after the events have been
872
- * dispatched (typically `markPersisted` handles it for you).
873
- */
874
- clearPendingEvents(): void;
875
- /**
876
- * **Framework lifecycle method — `@sealed`.** Called by `withCommit`
877
- * (or by your own orchestration code, after harvesting `pendingEvents`)
878
- * to push the persisted version back into the in-memory aggregate and
879
- * clear `pendingEvents`. TypeScript has no `final` keyword, but
880
- * subclasses **should not** override this method directly.
881
- *
882
- * Overriding without calling `super.markPersisted(version)` silently
883
- * leaks `pendingEvents` — the next `withCommit` will re-dispatch them
884
- * through the outbox, double-emitting events. This bug has been hit
885
- * in production by consumers; the {@link onPersisted} hook below is
886
- * the safer extension point.
887
- *
888
- * If you must override (legitimate cases are very rare), call
889
- * `super.markPersisted(version)` FIRST so the framework's cleanup
890
- * runs, then add your logic afterwards.
891
- *
892
- * @param version - The version assigned by the persistence layer
893
- * @see onPersisted — the safe extension point for subclasses
894
- */
895
- markPersisted(version: Version): void;
896
- /**
897
- * Subclass extension point — fires AFTER {@link markPersisted} has
898
- * updated the version and cleared `pendingEvents`. Override this for
899
- * post-persist logging, metrics, or cache-eviction without risk of
900
- * breaking the framework's pendingEvents cleanup.
901
- *
902
- * The default implementation is a no-op. Subclasses do NOT need to
903
- * call `super.onPersisted(version)` — there is nothing in the parent
904
- * implementation to preserve.
905
- *
906
- * **`onPersisted` deliberately receives only the version, not the
907
- * drained events.** Event-driven post-persist logic (aggregate-level
908
- * audit logging, per-event-type side effects) belongs in `EventBus`
909
- * subscribers or the outbox dispatcher — that is the proper
910
- * Aggregate-Boundary separation. Building event-aware logic into
911
- * `onPersisted` couples aggregate lifecycle to event processing and
912
- * recreates the boundary problems Vernon's aggregate discipline is
913
- * meant to prevent.
914
- *
915
- * **The hook must return synchronously.** `markPersisted` is `void`-
916
- * typed and calls `onPersisted` without `await`. TypeScript's
917
- * permissive `void` will accept an `async`-override returning
918
- * `Promise<void>`, but the returned promise is fire-and-forget —
919
- * any rejection becomes an unhandled rejection and `withCommit`
920
- * proceeds without waiting. For asynchronous work, subscribe to the
921
- * relevant domain event on the `EventBus` instead; that is the
922
- * properly awaited extension point.
923
- *
924
- * @param version - The version that was just persisted
925
- */
926
- protected onPersisted(_version: Version): void;
927
- /**
928
- * Mutates state and records the resulting domain events in the
929
- * **canonical record-after-mutation order**. Use this instead of calling
930
- * `setState` + `addDomainEvent` separately and you cannot trip the
931
- * "event for a fact that never happened" footgun.
932
- *
933
- * Order of operations:
934
- * 1. `setState(newState, true)` — runs `validateState` first.
935
- * If it throws, the method propagates and **no event is recorded
936
- * and no version is bumped**.
937
- * 2. Each event in `events` is appended via `addDomainEvent`.
938
- *
939
- * `commit()` **always bumps the version**, regardless of the aggregate's
940
- * `autoVersionBump` config. Recording a domain event implies "something
941
- * happened that the outside world cares about", and optimistic-
942
- * concurrency callers must see a fresh version every time. The config
943
- * still governs the un-coupled `setState` path. If you need to mutate
944
- * state without bumping (e.g. cosmetic caches), call `setState(newState,
945
- * false)` and skip `commit` entirely.
946
- *
947
- * `events` accepts a single event or an array. Omit it (or pass `[]`)
948
- * for state-only mutations.
949
- *
950
- * @example
951
- * ```ts
952
- * confirm(): void {
953
- * if (this.state.status === "confirmed") {
954
- * throw new OrderAlreadyConfirmedError(this.id);
955
- * }
956
- * this.commit(
957
- * { ...this.state, status: "confirmed" },
958
- * { type: "OrderConfirmed", orderId: this.id },
959
- * );
960
- * }
961
- * ```
962
- *
963
- * `EventSourcedAggregate.apply()` enforces the same ordering
964
- * structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
965
- * where `setState` and `addDomainEvent` are otherwise decoupled and the
966
- * ordering is convention-only.
967
- *
968
- * @param newState - The new state (validated by `validateState`)
969
- * @param events - One event, an array of events, or none (default)
970
- */
971
- protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
972
- protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
973
- /**
974
- * Records a domain event for later publication.
975
- *
976
- * **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
977
- * explicit: a domain event describes something that has just happened
978
- * to the aggregate — its existence implies the state change already
979
- * occurred. Concretely:
980
- *
981
- * ```ts
982
- * confirm(): void {
983
- * if (this.state.status === "confirmed") {
984
- * throw new OrderAlreadyConfirmedError(this.id);
985
- * }
986
- * this.setState({ ...this.state, status: "confirmed" }, true);
987
- * this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
988
- * // ↑ post-mutation. The event represents the committed fact.
989
- * }
990
- * ```
991
- *
992
- * Recording before mutation is a footgun: if a subsequent invariant
993
- * check throws, the event has already been queued but the state never
994
- * actually changed — consumers see an event for a fact that did not
995
- * happen.
996
- *
997
- * `EventSourcedAggregate.apply()` enforces this ordering structurally;
998
- * `AggregateRoot` leaves it as a convention because the state-mutation
999
- * path (`setState`) is decoupled from event recording.
1000
- *
1001
- * @param event - The domain event to record
1002
- */
1003
- protected addDomainEvent(event: TEvent): void;
1004
- /**
1005
- * Sugar for `createDomainEvent` that auto-injects `aggregateId`
1006
- * (from `this.id`) and `aggregateType` (from {@link aggregateType})
1007
- * into the event's metadata fields. This is the canonical path for
1008
- * recording events from inside aggregate domain methods.
1009
- *
1010
- * Downstream consumers — outbox dispatchers, projection handlers,
1011
- * audit logs — route by these two fields. Calling
1012
- * `createDomainEvent(...)` directly inside an aggregate method
1013
- * leaves them unset and is caught at the `withCommit` harvest
1014
- * boundary, but `this.recordEvent(...)` makes the right thing
1015
- * impossible to forget.
1016
- *
1017
- * @example
1018
- * ```ts
1019
- * class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
1020
- * protected readonly aggregateType = "Order";
1021
- *
1022
- * confirm(): void {
1023
- * this.commit(
1024
- * { ...this.state, status: "confirmed" },
1025
- * this.recordEvent("OrderConfirmed", { orderId: this.id }),
1026
- * );
1027
- * }
1028
- * }
1029
- * ```
1030
- *
1031
- * @param type - event type discriminator (must be one of `TEvent`'s tags)
1032
- * @param payload - payload for that event subtype
1033
- * @param options - any remaining `createDomainEvent` options
1034
- * (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
1035
- * and `aggregateType` are deliberately omitted — the helper sets
1036
- * them.
1037
- */
1038
- protected recordEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateDomainEventOptions, "aggregateId" | "aggregateType">): E;
1039
- /**
1040
- * Manually bumps the aggregate version.
1041
- * Call this after state changes for Optimistic Concurrency Control.
1042
- *
1043
- * If `autoVersionBump` is enabled, this is called automatically
1044
- * when using `setState()`.
1045
- */
1046
- protected bumpVersion(): void;
1047
- /**
1048
- * Sets the state and optionally bumps the version automatically.
1049
- * This is a convenience method for state mutations.
1050
- * Automatically validates the newState using `validateState()`.
1051
- * Overrides Entity.setState to add version bumping.
1052
- *
1053
- * @param newState - The new state
1054
- * @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
1055
- */
1056
- protected setState(newState: TState, bumpVersion?: boolean): void;
1057
- /**
1058
- * Creates a snapshot of the current aggregate state.
1059
- * Useful for performance optimization, backup/restore, and audit trails.
1060
- *
1061
- * @returns A snapshot containing the current state and version
945
+ * DB-baseline version. `undefined` until the aggregate has been
946
+ * persisted or restored at least once. Repository implementations
947
+ * route INSERT vs UPDATE on this field and use it as the OCC
948
+ * baseline. See `IRepository.save` JSDoc.
1062
949
  *
1063
- * @example
1064
- * ```typescript
1065
- * const snapshot = aggregate.createSnapshot();
1066
- * await snapshotRepository.save(aggregate.id, snapshot);
1067
- * ```
1068
- */
1069
- createSnapshot(): AggregateSnapshot<TState>;
1070
- /**
1071
- * Restores the aggregate from a snapshot.
1072
- * This is useful for loading aggregates from snapshots instead of
1073
- * rebuilding them from scratch.
1074
- * Validates the restored state.
1075
- *
1076
- * @param snapshot - The snapshot to restore from
1077
- *
1078
- * @example
1079
- * ```typescript
1080
- * const snapshot = await snapshotRepository.getLatest(aggregateId);
1081
- * aggregate.restoreFromSnapshot(snapshot);
1082
- * ```
1083
- */
1084
- restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
1085
- }
1086
-
1087
- /**
1088
- * Abstract base for **domain-invariant violations**. Domain methods
1089
- * (aggregates, entity validation hooks, value-object constructors)
1090
- * throw `DomainError`-derived exceptions when a business rule is
1091
- * violated. Consumers derive their own concrete errors — e.g.
1092
- * `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
1093
- * for `instanceof`-style catching at the App-Service boundary, where
1094
- * they typically map to HTTP 400 / business-rule responses.
1095
- *
1096
- * The library itself does **not** ship any concrete `DomainError`
1097
- * subclass — the kit can't know your invariants.
1098
- *
1099
- * Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
1100
- * surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
1101
- * `isRetryable`, …).
1102
- */
1103
- declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
1104
- }
1105
- /**
1106
- * Abstract base for **infrastructure / persistence failures** that the
1107
- * App-Service can recover from — typically by retrying, by returning
1108
- * HTTP 404 / 409, or by surfacing a "please try again" UX. These are
1109
- * not domain-invariant violations (the business rules were not
1110
- * broken); they describe race conditions and missing rows at the
1111
- * storage boundary.
1112
- *
1113
- * Library-internal concrete subclasses: {@link AggregateNotFoundError},
1114
- * {@link ConcurrencyConflictError}.
1115
- */
1116
- declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
1117
- }
1118
- /**
1119
- * Thrown by `EventSourcedAggregate.apply()` when no handler is
1120
- * registered for the event's type. This means the aggregate's subclass
1121
- * forgot to add an entry to its `handlers` map — a programming /
1122
- * configuration bug, not a domain or infrastructure failure.
1123
- *
1124
- * Deliberately **not** on `DomainError` or `InfrastructureError` —
1125
- * a generic `catch (e instanceof DomainError)` handler at the App
1126
- * layer must not mask a forgotten handler; this should crash loud and
1127
- * fail the calling Use Case so the bug surfaces in development. The
1128
- * replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
1129
- * also let it propagate uncaught instead of wrapping it in `Result.Err`.
1130
- *
1131
- * Use `isBaseError(e)` from `@shirudo/base-error` to detect
1132
- * "any structured error from the kit or any other BaseError-using
1133
- * library" at the App boundary.
1134
- */
1135
- declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
1136
- readonly eventType: string;
1137
- constructor(eventType: string, cause?: unknown);
1138
- }
1139
- /**
1140
- * Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
1141
- * given id does not exist. `InfrastructureError` because the storage
1142
- * boundary, not a business rule, decided the row is absent. Use the
1143
- * nullable variant `getById()` if "not found" is a valid outcome.
1144
- *
1145
- * Accepts an optional `cause` so a `Repository.save()` implementation
1146
- * can wrap a lower-level "row not found" / driver-level error without
1147
- * losing context. Cause-chain helpers (`getRootCause`,
1148
- * `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
1149
- *
1150
- * Not retryable — retrying won't make the row appear.
1151
- */
1152
- declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
1153
- readonly aggregateType: string;
1154
- readonly id: string;
1155
- constructor(aggregateType: string, id: string, cause?: unknown);
1156
- }
1157
- /**
1158
- * Thrown by `IRepository.save()` when the aggregate's expected version
1159
- * does not match the version currently persisted — i.e. another writer
1160
- * updated the aggregate concurrently. The canonical optimistic-
1161
- * concurrency signal; the App-Service typically reloads, re-applies
1162
- * the use case, and retries, or surfaces HTTP 409 to the caller.
1163
- *
1164
- * `InfrastructureError` because the persistence layer (not a domain
1165
- * rule) detects the race. Marks itself as `retryable: true` so the
1166
- * `isRetryable` predicate from `@shirudo/base-error` picks it up.
1167
- */
1168
- declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
1169
- readonly aggregateType: string;
1170
- readonly aggregateId: string;
1171
- readonly expectedVersion: number;
1172
- readonly actualVersion: number;
1173
- /**
1174
- * Marks this error as retryable so `isRetryable(err)` returns
1175
- * true. The canonical OCC pattern is to reload the aggregate, re-apply
1176
- * the use case, and retry on this exception.
1177
- */
1178
- readonly retryable: true;
1179
- constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
1180
- }
1181
-
1182
- /**
1183
- * Interface for Event-Sourced Aggregate Roots.
1184
- * Defines the contract for aggregates that manage state changes via event sourcing.
1185
- *
1186
- * @template TId - The type of the aggregate root identifier
1187
- * @template TEvent - The union type of all domain events
1188
- */
1189
- interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
1190
- /**
1191
- * Reconstitutes the aggregate from an event history. Returns `Result`
1192
- * because event-stream corruption is an expected recoverable failure
1193
- * at the infrastructure boundary.
1194
- *
1195
- * @param history - An ordered list of past events
1196
- */
1197
- loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
1198
- }
1199
- type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
1200
- /**
1201
- * Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
1202
- *
1203
- * Like `AggregateRoot`, this is both the root entity and the aggregate boundary.
1204
- * The difference is persistence: state is derived from events, not stored directly.
1205
- * Events are the single source of truth — all state changes go through `apply()` → handler.
1206
- *
1207
- * Extends `Entity` directly (not `AggregateRoot`) so that `setState()` and
1208
- * `addDomainEvent()` are not available. This enforces the event sourcing pattern
1209
- * at the type level — there is no way to mutate state without going through an event handler.
1210
- *
1211
- * `apply()` and `validateEvent()` throw `DomainError`-derived exceptions on
1212
- * invariant violations. Subclasses override `validateEvent()` to throw their
1213
- * own concrete subclasses (e.g. `OrderAlreadyConfirmedError`). Only the
1214
- * infrastructure-boundary methods (`loadFromHistory`,
1215
- * `restoreFromSnapshotWithEvents`) return `Result` — they catch `DomainError`
1216
- * during replay so callers can react to corrupted event streams without
1217
- * try/catch.
1218
- *
1219
- * @template TState - The type of the aggregate state (contains child entities and value objects)
1220
- * @template TEvent - The union type of all domain events
1221
- * @template TId - The type of the aggregate root identifier
1222
- *
1223
- * @example
1224
- * ```typescript
1225
- * class OrderAlreadyConfirmedError extends DomainError {
1226
- * constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
1227
- * }
1228
- *
1229
- * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
1230
- * confirm(): void {
1231
- * this.apply(createDomainEvent("OrderConfirmed", {}));
1232
- * }
1233
- *
1234
- * protected validateEvent(event: OrderEvent): void {
1235
- * if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
1236
- * throw new OrderAlreadyConfirmedError(this.id);
1237
- * }
1238
- * }
1239
- *
1240
- * protected readonly handlers = {
1241
- * OrderConfirmed: (state: OrderState): OrderState => ({
1242
- * ...state,
1243
- * status: "confirmed",
1244
- * }),
1245
- * };
1246
- * }
1247
- * ```
1248
- */
1249
- declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
950
+ * Distinct from {@link version}, which is the in-memory
951
+ * post-mutation value. Mutations bump `_version` but never touch
952
+ * `_persistedVersion` that field only moves on {@link markRestored}
953
+ * (Post-Load) and {@link markPersisted} (Post-Save).
954
+ */
955
+ private _persistedVersion;
956
+ private _pendingEvents;
957
+ get version(): Version;
958
+ get persistedVersion(): Version | undefined;
1250
959
  /**
1251
- * The aggregate's domain type as a string, used to populate
1252
- * `aggregateType` on events recorded via {@link recordEvent}.
960
+ * Read-only list of domain events recorded on this aggregate that
961
+ * have not yet been flushed to the outbox / persistence layer.
962
+ */
963
+ get pendingEvents(): ReadonlyArray<TEvent>;
964
+ /**
965
+ * Clears the pending-event list. Called by `markPersisted` after a
966
+ * successful write — the events have been handed off to the outbox
967
+ * / event store and are no longer the aggregate's responsibility.
968
+ */
969
+ clearPendingEvents(): void;
970
+ protected setVersion(version: Version): void;
971
+ /**
972
+ * Manually bumps the aggregate version. Used by state-stored
973
+ * aggregates' `setState(_, true)` / `commit()` paths and by the
974
+ * event-sourced replay path after each applied event.
975
+ */
976
+ protected bumpVersion(): void;
977
+ /**
978
+ * **Lifecycle marker — Post-Load.** Syncs both `_version` and
979
+ * `_persistedVersion` to the DB-stored version. Used by
980
+ * `reconstitute(...)` factories to assemble an in-memory aggregate
981
+ * from a persisted row.
1253
982
  *
1254
- * Subclasses MUST declare this as a string literal:
983
+ * Does NOT fire {@link onPersisted} that hook has post-save
984
+ * semantics (metrics, audit, cache eviction), not post-load. The
985
+ * Factory-vs-Reconstitution distinction (Vernon §11) is honoured
986
+ * structurally: two separate markers, one for each transition.
987
+ *
988
+ * @param version - The version the row currently holds in the DB
1255
989
  *
990
+ * @example
1256
991
  * ```ts
1257
- * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
1258
- * protected readonly aggregateType = "Order";
992
+ * static reconstitute(id: OrderId, state: OrderState, version: Version): Order {
993
+ * const order = new Order(id, state);
994
+ * order.markRestored(version);
995
+ * return order;
1259
996
  * }
1260
997
  * ```
1261
- *
1262
- * Downstream consumers (outbox dispatchers, projection handlers,
1263
- * audit logs) route by this. Use the canonical aggregate name
1264
- * consistently across your bounded context. The value comes from
1265
- * this explicit declaration, not `constructor.name` (fragile under
1266
- * minification + bundler transforms).
1267
998
  */
1268
- protected abstract readonly aggregateType: string;
1269
- private _version;
1270
- get version(): Version;
1271
- private setVersion;
1272
- private _pendingEvents;
1273
- get pendingEvents(): ReadonlyArray<TEvent>;
1274
- clearPendingEvents(): void;
999
+ protected markRestored(version: Version): void;
1275
1000
  /**
1276
1001
  * **Framework lifecycle method — `@sealed`.** Called by `withCommit`
1277
1002
  * (or by your own orchestration code, after harvesting `pendingEvents`)
@@ -1324,32 +1049,240 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1324
1049
  * @param version - The version that was just persisted
1325
1050
  */
1326
1051
  protected onPersisted(_version: Version): void;
1327
- protected constructor(id: TId, initialState: TState);
1052
+ /**
1053
+ * Appends a domain event to the pending list. Prefer the higher-level
1054
+ * `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
1055
+ * (event-sourced) call sites — both wrap `addDomainEvent` in the
1056
+ * canonical record-AFTER-mutation order (Vernon §8). Calling
1057
+ * `addDomainEvent` directly is appropriate only when state and event
1058
+ * recording have already been decoupled deliberately (e.g. a
1059
+ * deletion event before a hard-delete; see `docs/guide/repository.md`).
1060
+ */
1061
+ protected addDomainEvent(event: TEvent): void;
1062
+ /**
1063
+ * Creates a snapshot of the current aggregate state — the state at
1064
+ * this moment plus the version. Useful for ES snapshot policies and
1065
+ * for state-stored backup / restore.
1066
+ */
1067
+ createSnapshot(): AggregateSnapshot<TState>;
1328
1068
  /**
1329
1069
  * Sugar for `createDomainEvent` that auto-injects `aggregateId`
1330
1070
  * (from `this.id`) and `aggregateType` (from {@link aggregateType})
1331
- * into the event's metadata fields. The canonical path for
1332
- * constructing events to feed into `apply()` from inside aggregate
1333
- * domain methods.
1071
+ * into the event's metadata fields. This is the canonical path for
1072
+ * recording events from inside aggregate domain methods.
1073
+ *
1074
+ * Downstream consumers — outbox dispatchers, projection handlers,
1075
+ * audit logs — route by these two fields. Calling
1076
+ * `createDomainEvent(...)` directly inside an aggregate method
1077
+ * leaves them unset and is caught at the `withCommit` harvest
1078
+ * boundary, but `this.recordEvent(...)` makes the right thing
1079
+ * impossible to forget.
1334
1080
  *
1335
1081
  * @example
1336
1082
  * ```ts
1337
- * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
1083
+ * class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
1338
1084
  * protected readonly aggregateType = "Order";
1339
1085
  *
1340
1086
  * confirm(): void {
1341
- * this.apply(this.recordEvent("OrderConfirmed", { orderId: this.id }));
1087
+ * this.commit(
1088
+ * { ...this.state, status: "confirmed" },
1089
+ * this.recordEvent("OrderConfirmed", { orderId: this.id }),
1090
+ * );
1342
1091
  * }
1343
1092
  * }
1344
1093
  * ```
1345
1094
  *
1346
- * Calling `createDomainEvent(...)` directly inside an aggregate
1347
- * method leaves `aggregateId` and `aggregateType` unset; the
1348
- * `withCommit` harvest boundary catches it at runtime, but
1349
- * `this.recordEvent(...)` makes the right thing impossible to
1350
- * forget.
1095
+ * @param type - event type discriminator (must be one of `TEvent`'s tags)
1096
+ * @param payload - payload for that event subtype
1097
+ * @param options - any remaining `createDomainEvent` options
1098
+ * (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
1099
+ * and `aggregateType` are deliberately omitted — the helper sets
1100
+ * them.
1351
1101
  */
1352
1102
  protected recordEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateDomainEventOptions, "aggregateId" | "aggregateType">): E;
1103
+ }
1104
+
1105
+ /**
1106
+ * Configuration options for AggregateRoot behavior.
1107
+ */
1108
+ interface AggregateConfig {
1109
+ /**
1110
+ * Whether `setState()` should bump the version automatically when the
1111
+ * caller omits the per-call `bumpVersion` argument.
1112
+ *
1113
+ * Defaults to **`false`** — `setState()` already takes an explicit
1114
+ * `bumpVersion` argument per call, so the config is just the default
1115
+ * the per-call argument falls back to. Set to `true` only if you have
1116
+ * a subclass that never passes `bumpVersion` and you want every state
1117
+ * change to advance the version anyway.
1118
+ */
1119
+ autoVersionBump?: boolean;
1120
+ }
1121
+ /**
1122
+ * Base class for Aggregate Roots without Event Sourcing.
1123
+ *
1124
+ * In DDD (Evans), an Aggregate is a cluster of objects — root entity, child entities,
1125
+ * and value objects — treated as a unit for consistency. The **Aggregate Root** is the
1126
+ * root entity that represents the aggregate externally and is the only entry point
1127
+ * for external code. This class serves as both: it IS the root entity and it contains
1128
+ * the aggregate state (`TState`) which holds child entities and value objects.
1129
+ *
1130
+ * Provides:
1131
+ * - Identity (id) and state management (via `Entity`)
1132
+ * - Version + persistedVersion + pending-event tracking (via `BaseAggregate`)
1133
+ * - `setState`-based mutation with optional version bumping
1134
+ * - `commit()` record-after-mutation helper
1135
+ * - Snapshot support for performance optimization
1136
+ *
1137
+ * All changes to child entities within `TState` are versioned through this root.
1138
+ * Use `setState()` for state mutations to ensure invariant validation.
1139
+ *
1140
+ * For event sourcing, use `EventSourcedAggregate` instead.
1141
+ *
1142
+ * @template TState - The type of the aggregate state (contains child entities and value objects)
1143
+ * @template TId - The type of the aggregate root identifier
1144
+ * @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never` — aggregates without a declared event type cannot emit events (emitting any event becomes a compile error). Supply a concrete event union to opt in.
1145
+ *
1146
+ * @example
1147
+ * ```typescript
1148
+ * // Order is an Aggregate Root (an Entity with version)
1149
+ * class Order extends AggregateRoot<OrderState, OrderId> {
1150
+ * protected readonly aggregateType = "Order";
1151
+ *
1152
+ * constructor(id: OrderId, initialState: OrderState) {
1153
+ * super(id, initialState);
1154
+ * }
1155
+ *
1156
+ * confirm(): void {
1157
+ * this.commit(
1158
+ * { ...this.state, status: "confirmed" },
1159
+ * this.recordEvent("OrderConfirmed", { orderId: this.id }),
1160
+ * );
1161
+ * }
1162
+ * }
1163
+ * ```
1164
+ */
1165
+ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends BaseAggregate<TState, TId, TEvent> {
1166
+ private readonly _autoVersionBump;
1167
+ protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
1168
+ /**
1169
+ * Mutates state and records the resulting domain events in the
1170
+ * **canonical record-after-mutation order**. Use this instead of calling
1171
+ * `setState` + `addDomainEvent` separately and you cannot trip the
1172
+ * "event for a fact that never happened" footgun.
1173
+ *
1174
+ * Order of operations:
1175
+ * 1. `setState(newState, true)` — runs `validateState` first.
1176
+ * If it throws, the method propagates and **no event is recorded
1177
+ * and no version is bumped**.
1178
+ * 2. Each event in `events` is appended via `addDomainEvent`.
1179
+ *
1180
+ * `commit()` **always bumps the version**, regardless of the aggregate's
1181
+ * `autoVersionBump` config. Recording a domain event implies "something
1182
+ * happened that the outside world cares about", and optimistic-
1183
+ * concurrency callers must see a fresh version every time. The config
1184
+ * still governs the un-coupled `setState` path. If you need to mutate
1185
+ * state without bumping (e.g. cosmetic caches), call `setState(newState,
1186
+ * false)` and skip `commit` entirely.
1187
+ *
1188
+ * `events` accepts a single event or an array. Omit it (or pass `[]`)
1189
+ * for state-only mutations.
1190
+ *
1191
+ * @example
1192
+ * ```ts
1193
+ * confirm(): void {
1194
+ * if (this.state.status === "confirmed") {
1195
+ * throw new OrderAlreadyConfirmedError(this.id);
1196
+ * }
1197
+ * this.commit(
1198
+ * { ...this.state, status: "confirmed" },
1199
+ * this.recordEvent("OrderConfirmed", { orderId: this.id }),
1200
+ * );
1201
+ * }
1202
+ * ```
1203
+ *
1204
+ * `EventSourcedAggregate.apply()` enforces the same ordering
1205
+ * structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
1206
+ * where `setState` and `addDomainEvent` are otherwise decoupled and the
1207
+ * ordering is convention-only.
1208
+ *
1209
+ * @param newState - The new state (validated by `validateState`)
1210
+ * @param events - One event, an array of events, or none (default)
1211
+ */
1212
+ protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
1213
+ /**
1214
+ * Sets the state and optionally bumps the version automatically.
1215
+ * Validates `newState` via `validateState()`.
1216
+ *
1217
+ * @param newState - The new state
1218
+ * @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
1219
+ */
1220
+ protected setState(newState: TState, bumpVersion?: boolean): void;
1221
+ /**
1222
+ * Restores the aggregate from a snapshot — loads state and aligns
1223
+ * `version` + `persistedVersion` to the snapshot version. Validates
1224
+ * the restored state.
1225
+ *
1226
+ * @param snapshot - The snapshot to restore from
1227
+ */
1228
+ restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
1229
+ }
1230
+
1231
+ type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
1232
+ /**
1233
+ * Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
1234
+ *
1235
+ * Like `AggregateRoot`, this is both the root entity and the aggregate
1236
+ * boundary. The difference is persistence: state is derived from events,
1237
+ * not stored directly. Events are the single source of truth — all state
1238
+ * changes go through `apply()` → handler.
1239
+ *
1240
+ * Extends `BaseAggregate` (the shared lifecycle machinery) but does NOT
1241
+ * expose `setState()` or `commit()` from `AggregateRoot`. This enforces
1242
+ * the event sourcing pattern at the type level — there is no way to
1243
+ * mutate state without going through an event handler.
1244
+ *
1245
+ * `apply()` and `validateEvent()` throw `DomainError`-derived exceptions
1246
+ * on invariant violations. Subclasses override `validateEvent()` to
1247
+ * throw their own concrete subclasses (e.g. `OrderAlreadyConfirmedError`).
1248
+ * Only the infrastructure-boundary methods (`loadFromHistory`,
1249
+ * `restoreFromSnapshotWithEvents`) return `Result` — they catch
1250
+ * `DomainError` during replay so callers can react to corrupted event
1251
+ * streams without try/catch.
1252
+ *
1253
+ * @template TState - The aggregate state (contains child entities and value objects)
1254
+ * @template TEvent - The union type of all domain events
1255
+ * @template TId - The aggregate root identifier
1256
+ *
1257
+ * @example
1258
+ * ```typescript
1259
+ * class OrderAlreadyConfirmedError extends DomainError {
1260
+ * constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
1261
+ * }
1262
+ *
1263
+ * class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
1264
+ * protected readonly aggregateType = "Order";
1265
+ *
1266
+ * confirm(): void {
1267
+ * this.apply(this.recordEvent("OrderConfirmed", { orderId: this.id }));
1268
+ * }
1269
+ *
1270
+ * protected validateEvent(event: OrderEvent): void {
1271
+ * if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
1272
+ * throw new OrderAlreadyConfirmedError(this.id);
1273
+ * }
1274
+ * }
1275
+ *
1276
+ * protected readonly handlers = {
1277
+ * OrderConfirmed: (state: OrderState): OrderState => ({
1278
+ * ...state,
1279
+ * status: "confirmed",
1280
+ * }),
1281
+ * };
1282
+ * }
1283
+ * ```
1284
+ */
1285
+ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends BaseAggregate<TState, TId, TEvent> implements IEventSourcedAggregate<TId, TEvent> {
1353
1286
  /**
1354
1287
  * Validates an event before it is applied. Default is no-op.
1355
1288
  * Subclasses override to throw a concrete `DomainError` subclass when
@@ -1397,10 +1330,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1397
1330
  * 2 events ends at v=3, not v=2.
1398
1331
  */
1399
1332
  loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
1400
- /**
1401
- * Creates a snapshot of the current aggregate state.
1402
- */
1403
- createSnapshot(): AggregateSnapshot<TState>;
1404
1333
  /**
1405
1334
  * Restores the aggregate from a snapshot and applies events that occurred
1406
1335
  * after. Same infrastructure-boundary semantics as `loadFromHistory`:
@@ -1423,56 +1352,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1423
1352
  };
1424
1353
  }
1425
1354
 
1426
- type Version = number & {
1427
- readonly __v: true;
1428
- };
1429
- /**
1430
- * Snapshot of an aggregate state at a specific point in time.
1431
- * Used for optimizing event replay by starting from a snapshot
1432
- * instead of replaying all events from the beginning.
1433
- *
1434
- * @template TState - The type of the aggregate state
1435
- */
1436
- interface AggregateSnapshot<TState> {
1437
- /**
1438
- * The state of the aggregate at the time of the snapshot.
1439
- */
1440
- state: TState;
1441
- /**
1442
- * The version of the aggregate when the snapshot was taken.
1443
- */
1444
- version: Version;
1445
- /**
1446
- * Timestamp when the snapshot was created.
1447
- */
1448
- snapshotAt: Date;
1449
- }
1450
- /**
1451
- * Checks if two aggregates are at the same version (same ID and version).
1452
- * Useful for optimistic concurrency control checks.
1453
- *
1454
- * Note: Two aggregates with the same ID ARE the same aggregate (identity).
1455
- * This function checks if they are at the same version — i.e., no concurrent modification.
1456
- *
1457
- * @example
1458
- * ```typescript
1459
- * const before = await repository.getById(id);
1460
- * // ... some operations ...
1461
- * const after = await repository.getById(id);
1462
- *
1463
- * if (!sameVersion(before, after)) {
1464
- * throw new Error("Aggregate was modified by another process");
1465
- * }
1466
- * ```
1467
- */
1468
- declare function sameVersion<TId extends Id<string>>(a: {
1469
- id: TId;
1470
- version: Version;
1471
- }, b: {
1472
- id: TId;
1473
- version: Version;
1474
- }): boolean;
1475
-
1476
1355
  /**
1477
1356
  * Marker interface for Commands.
1478
1357
  * Commands represent write operations that change system state.
@@ -2329,26 +2208,25 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
2329
2208
  * stored (optimistic concurrency).
2330
2209
  * 2. Write the aggregate to durable storage.
2331
2210
  *
2332
- * **Insert vs update — library convention.** A fresh aggregate begins
2333
- * at `version === 0` (the `Version` brand defaults to `0` in both
2334
- * `AggregateRoot` and `EventSourcedAggregate`). After the first
2335
- * versioned mutation (`setState(_, true)`, `apply()`, `commit()`) the
2336
- * version is `> 0`. Implementations distinguish the two paths by the
2337
- * incoming `aggregate.version`:
2211
+ * **Insert vs update — `persistedVersion` convention.** Every aggregate
2212
+ * exposes two version fields with distinct roles:
2213
+ *
2214
+ * - `aggregate.version` — in-memory post-mutation value, bumped by
2215
+ * `setState(_, true)` / `commit()` / `apply()`. NOT the right
2216
+ * routing key, because mutations can advance it past zero while
2217
+ * the DB row still does not exist.
2218
+ * - `aggregate.persistedVersion` — what the persistence layer holds.
2219
+ * `undefined` until the aggregate has been persisted or restored
2220
+ * at least once. This is the routing key.
2338
2221
  *
2339
- * - `aggregate.version === 0` → **INSERT** (no existing row to lock
2340
- * against; the write succeeds unconditionally or fails the unique
2222
+ * - `aggregate.persistedVersion === undefined` → **INSERT** (never
2223
+ * persisted; write succeeds unconditionally or fails the unique
2341
2224
  * constraint on `id`).
2342
- * - `aggregate.version > 0` → **UPDATE** with the OCC predicate
2343
- * `WHERE id = ? AND version = expected`. If the row count is `0`,
2344
- * another writer raced you throw `ConcurrencyConflictError`.
2345
- *
2346
- * The library does not formalise this in the type system because
2347
- * version-bump semantics differ across the two aggregate flavours
2348
- * (state-stored aggregates bump on the user's call to `setState(_,
2349
- * true)`; event-sourced aggregates bump on every `apply()` by
2350
- * definition). The `version === 0` invariant for "never persisted" is
2351
- * the common contract.
2225
+ * - otherwise → **UPDATE** with the OCC predicate
2226
+ * `WHERE id = ? AND version = aggregate.persistedVersion` (the
2227
+ * load-time / last-save baseline, not the post-mutation in-memory
2228
+ * value). If the row count is `0`, another writer raced you —
2229
+ * throw `ConcurrencyConflictError`.
2352
2230
  *
2353
2231
  * Do **not** call `aggregate.markPersisted(...)` here. The library's
2354
2232
  * `withCommit` orchestrator handles the post-save lifecycle (harvest