immutable-domain-entities
Immutable Domain Entities
Goal
Apply the immutable design pattern when writing or changing domain entities.
An immutable domain entity keeps the same domain identity while preventing in-place mutation of its state. Domain operations that would otherwise change the entity must instead return a new entity instance, or the closest immutable equivalent the stack supports, preserving the same identity and enforcing the same invariants.
Treat this skill as construction and change-management guidance for domain entities. The key question is whether the entity can be modeled so its state is fixed after creation and all valid changes are expressed as explicit creation of a new immutable version.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- defines a domain entity's fields, properties, or constructor
- defines domain operations that would change a domain entity's state
- exposes getters, setters, collections, or nested objects on a domain entity
- loads, reconstructs, or rehydrates domain entities from persistence or transport data
- copies, clones, replaces, or updates domain entity instances
- enforces invariants during creation or state transitions of domain entities
Immutable Rule
-
Model domain entities as immutable when the task writes or changes them.
- Use immutable classes or the closest immutable class-like construct the language supports.
- A plain type alias, interface, or record paired with standalone functions is not a class-like construct. Do not model an immutable domain entity that way.
- Keep the entity's identity stable while returning a new entity instance for every valid state change.
-
Do not mutate entity state in place.
- Do not add public setters, writable public fields, or in-place mutation methods.
- Do not let domain methods silently change internal state on the existing entity instance.
-
Keep all entity data effectively immutable.
- Make fields readonly or the closest equivalent when the stack supports it.
- Prevent mutation through exposed references, including collections, maps, nested objects, or buffers.
- Copy or freeze mutable inputs and outputs when needed to preserve immutability.
-
Express change through intention-revealing operations.
- Domain operations such as
changeAddress,approve,cancel, orassignOwnershould return a new entity with the same identity and the updated valid state. - Preserve invariant checks in those operations instead of pushing validation to callers.
- Domain operations such as
-
Keep framework constraints at the boundary.
- If a persistence or framework layer requires a mutable representation, keep that requirement outside the domain entity when possible.
- Preserve an immutable domain entity model even if boundary adapters need to map to or from another shape.
Detection Workflow
-
Find the mutation surface first.
- Identify setters, writable fields, mutable collections, direct property assignment, and methods that update state in place.
- Identify whether callers can change the entity without going through an explicit domain operation.
-
Trace identity-preserving changes.
- Determine which operations are meant to evolve the same domain concept over time.
- Verify that those changes can be represented by returning a new instance with the same identity.
-
Check nested mutability.
- Inspect collections, child objects, and other referenced data carried by the entity.
- Verify that immutability is not broken through aliases to mutable internal data.
-
Prefer semantic classification to framework conventions.
- Do not assume a mutable ORM or framework model should also dictate the domain entity shape.
- Classify and design the entity by domain needs first, then adapt boundaries as needed.
Writing or Changing Immutable Domain Entities
-
Make construction explicit.
- Use constructors that fully establish a valid immutable entity.
- Validate invariants at creation time.
-
Return new instances for state changes.
- Replace in-place updates with methods that produce a new entity value carrying the same identity.
- Keep method names aligned with domain intent rather than generic copy or patch semantics when possible.
-
Eliminate mutable write paths.
- Remove or avoid public setters and writable public fields.
- Replace mutation-oriented helpers with copy, replace, or transition methods that preserve invariants.
-
Protect internal references.
- Use immutable collections or defensive copying when the language requires it.
- Do not expose mutable internal references that let callers bypass entity rules.
-
Preserve rehydration and reconstruction safely.
- Ensure entities loaded from persistence or transport can still be reconstructed as immutable objects.
- Keep rehydration paths explicit enough to preserve identity and invariants.
Review Questions
When reading or reviewing code, ask:
- Can this domain entity be mutated after construction?
- Do state-changing operations return a new instance with the same identity?
- Are invariants preserved in the creation and transition methods?
- Can callers mutate the entity through setters, writable fields, or exposed mutable references?
- Would changing this code weaken immutability for a domain entity?
If the answer is yes, apply this skill.
Report the Outcome
When finishing the task:
- state which domain entities were identified or changed
- state which immutable constructs or patterns were used
- state how identity-preserving updates were implemented
- state which mutable writing paths or exposed mutable references were removed or prevented
More from code-sherpas/agent-skills
neverthrow-return-types
Require `neverthrow`-based return types in TypeScript and JavaScript code whenever the surrounding technology allows it. Use when creating, refactoring, reviewing, or extending standalone functions, exported module functions, class methods, object methods, service methods, repository methods, and similar APIs that should expose explicit success and failure result types in their signatures. Prefer `Result<T, E>` for synchronous code and `ResultAsync<T, E>` for asynchronous code. Only skip a `neverthrow` return type when a framework, library, runtime interface, or externally imposed contract is incompatible and requires a different return shape.
16neverthrow-wrap-exceptions
Capture exceptions and promise failures with `neverthrow` instead of hand-written `try/catch` in TypeScript and JavaScript code. Use when wrapping synchronous functions that may throw, promise-returning functions that may throw before returning, existing `PromiseLike` values that may reject, or third-party APIs such as parsers, database clients, HTTP clients, file-system helpers, serializers, and SDK calls. Prefer `Result.fromThrowable` for synchronous throwers, `ResultAsync.fromThrowable` for promise-returning functions that may throw or reject, and `ResultAsync.fromPromise` when you already have a `PromiseLike` value in hand. Only keep `try/catch` when the language construct, cleanup requirement, or framework boundary truly requires it.
11atomic-design
Create or update web UI components with a strict reuse-first workflow. Use when building, refactoring, restyling, or extending frontend or template components while minimizing raw DOM or HTML by reusing or generalizing existing components first.
10write-persistence-representations
Create or update persistence-layer data representations in any stack, including ORM entities, schema definitions, table mappings, document models, collection definitions, and similar database-facing code. Use when agents needs to add or change persisted fields, identifiers, relationships, indexes, timestamps, auditing fields, or storage mappings in frameworks, libraries, or ORMs such as Prisma, TypeORM, Sequelize, Drizzle, Mongoose, Hibernate/JPA, Doctrine, Ecto, Active Record, or equivalent persistence technologies.
7business-logic
Identify, interpret, review, or write business logic in code. Use when an agent needs to decide whether code expresses business rules, business algorithms, or business workflows, or when it must implement, preserve, or refactor code that creates, stores, or transforms data according to real business policies.
7update-agent-skills
Update agent skills installed with the `skills` CLI. Use when asked to refresh installed skills, keep a project's skills current, or troubleshoot cases where `npx skills update` reports that everything is up to date. For project-scoped installs, a no-change update must immediately run the bundled reinstall script so tracked skills from `skills-lock.json` are reinstalled without extra investigation.
7