What is Change Tracking?
Change tracking is implemented byBaseEntity, so it is available to both
Entity and Aggregate. JavaScript Proxies observe property assignments and
collection mutations, while snapshots are used to calculate create, update, and
delete operations for nested entities.
In normal DDD usage, call getChanges() on the aggregate root so the returned
operations represent a single consistency and persistence boundary.
Efficient Persistence
Persist detected updates instead of rewriting every child
Zero Boilerplate
Changes tracked automatically - no manual code needed
Correct Ordering
Creates and deletes ordered by object-graph depth
Batch Operations
Group changes by entity type for optimized database operations
Construction establishes the initial baseline.
getChanges() reports
mutations made after construction or after the most recent
markAsClean()/markAsPersisted() call. It does not emit a create operation
for the root object itself; repositories normally use isNew() for that
decision.Basic Usage
Getting Changes
Accessing Operations
Filtering by Entity
Get changes for a specific entity type:Filtering by Relation
UseforRelation() to select operations associated with a property in the
parent object:
Excluding Entities
Usewithout() when you need the inverse of of() — all changes except one or more entity types. It returns a new AggregateChanges instance; the original is unchanged.
onUpdate implementations when one entity needs manual persistence (nested writes, custom PK, etc.) and the rest should go through PrismaBatchExecutor.
Resetting the Baseline
The component that successfully persists the object graph should reset its tracking baseline:markAsClean() recursively clears changes in nested entities without changing
whether their IDs are new.
PrismaRepository.save() already calls markAsPersisted() after its mapper
completes successfully. Do not add a second cleanup call when using that
repository. For a custom repository or direct batch executor, reset the
baseline only after persistence succeeds.After First Save
When implementing a custom repository, callmarkAsPersisted() after the first
successful INSERT. It clears tracking and marks the root and nested IDs as not
new:
markAsPersisted() changes ID state; it does not perform an INSERT by itself.
The repository or mapper decides how isNew() affects persistence.Batch Operations
UsetoBatchOperations() to group changes by entity, relation, and parent
metadata:
toBatchOperations() is a data transformation only. It does not execute a
transaction, map domain fields to database columns, or decide whether a
relation is owned or referenced. A persistence adapter must handle those
concerns.The ordering reflects the tracked object graph:
- Deletes: Children first, then parents
- Creates: Parents first, then children
- Updates: Grouped by entity without depth ordering
Operation Ordering
The library orders create and delete operations by their depth in the tracked object graph. Depth0 is the object on which getChanges() is called. The tracker can emit
an update for that root, but it does not emit a create or delete for the root
itself.
Deletes: Leaf → Root
Children must be deleted before parents to avoid FK violations:Creates: Root → Leaf
Parents must be created before children so FKs can reference them:Updates: Any Order
Updates are returned in detection order and grouped by entity in batch output:Working with Collections
Collections ofEntity/Aggregate instances produce item-level create,
update, and delete operations. Arrays containing only primitive values are
reported as an update to the property of their owning entity.
Adding Items
Removing Items
Updating Items
Mixed Operations
Working with Single Entities (1:1)
Setting an Entity
Removing an Entity
Updating an Entity
Replacing an Entity
Deeply Nested Changes
Change tracking recursively follows nestedBaseEntity instances and has no
fixed depth limit:
Cascading Deletes
When a parent is removed from the tracked graph, its nested entities are also reported as delete operations:ON DELETE CASCADE. The adapter still decides whether a relation is owned and should be
deleted or is a reference that should only be disconnected.
Cascading Creates
When a parent with nested entities is added after the baseline, the parent and its nested children are reported as creates:Change History
getHistory() returns low-level writes observed by the entity’s tracker:
AggregateChanges: multiple writes may collapse into one update operation,
and changes reverted back to the baseline may produce no persistence operation.
Collections with Entities
Use entities when collection items need independent create, update, and delete operations. Value Objects remain appropriate for immutable values that are persisted as part of their owner:How Entity Tracking Works
Entities are tracked by their ID:Nested Entities Example
Type-Safe Changes
For better TypeScript support, define an entity map:Helper Method Pattern
Entity-map keys must match the runtime class names stored in operations. For
example, an
Address instance produces the entity name "Address", not the
property name "shippingAddress".Persistence Example with Prisma
PrismaToPersistence uses PrismaBatchExecutor for updates by default. The
executor understands relation metadata, schema mappings, owned collections, and
reference collections, while PrismaRepository.save() resets the aggregate
with markAsPersisted() after persistence:
PrismaToPersistence.onUpdate() only when part of the graph needs
custom persistence. Use without() to pass the remaining operations to the
default executor.
API Reference
BaseEntity Tracking
These methods are available on bothEntity and Aggregate: