Value Objects
A Value Object represents a domain concept that is immutable and has no conceptual identity. Unlike entities, value objects are defined purely by their attributes. They are used to describe characteristics of an entity and often encapsulate logic related to validation or domain-specific constraints.
For example, an Account Name can be modeled as a Value Object since it is defined by its value and doesn’t need a unique identity.
Creating a Value Object
Section titled “Creating a Value Object”In this library, you can create a Value Object by extending the ValueObject class. Value Objects are expected to be immutable and should encapsulate any validation or business rules required for their creation.
import { ValueObject } from '@ocoda/event-sourcing';
export class AccountName extends ValueObject { public static fromString(name: string) { if(name.length < 3) { throw new Error('Account name should contain at least 3 characters'); } return new AccountName({ value: name }); }
get value(): string { return this.props.value; }}By extending the ValueObject class, you get access to the equals method, which compares two Value Objects based on their properties. This method is used to determine if two Value Objects are in fact equal.
Ids are value objects too. Extend UUID or ULID for generated ids, or Id for ids of your own format:
import { ULID, UUID } from '@ocoda/event-sourcing';
export class AccountId extends UUID {}export class AccountOwnerId extends UUID {}export class OrderId extends ULID {}
const accountId = AccountId.generate(); // an AccountIdconst sameAccountId = AccountId.from(accountId.value); // an AccountIdconst ownerIds = ['b6bca415-b7a6-499c-9f39-bf8fbf980a82'].map(AccountOwnerId.from); // AccountOwnerIds, typed as UUID[]generate()andfrom()create an instance of the class they are called on (since 4.0; 3.x returned aUUIDorULID), also when they are passed around, as invalues.map(AccountOwnerId.from). Passed around, a factory is typed as the base class, soownerIdsis aUUID[]that holdsAccountOwnerIds: writevalues.map((value) => AccountOwnerId.from(value))for anAccountOwnerId[].ULID.factory()returns a generator of ids that increase monotonically.- Ids of different classes are never equal, even with the same value:
AccountId.from(value).equals(AccountOwnerId.from(value))isfalse. - The factories call
new this(value). An id class whose constructor takes anything other than the id alone declares its owngenerate()andfrom(), and a constructor of your own isprotected, notprivate. from()throws anInvalidIdExceptionfor an empty value or a value that doesn’t have the format of the class. AUUIDis 32 hexadecimal digits with its four dashes, in either case. AULIDis 26 characters of Crockford’s base32 in either case (the digits and the letters without I, L, O and U), starting with 0 to 7 (since 4.0; 3.x accepted any 26 letters and digits).
Guidelines for working with Value Objects
Section titled “Guidelines for working with Value Objects”-
Immutability
Section titled “Immutability”Value Objects are immutable, meaning once they are created, they cannot be modified. This ensures consistency and prevents unintended side effects. If a change is needed, you should create a new instance of the Value Object rather than modifying an existing one. For example, if an account name changes, a new AccountName object should be created rather than mutating the existing one. That’s why we provide a ValueObject class to extend, in the background this will freeze the object to prevent any changes.
-
Validation in creation
Section titled “Validation in creation”Value Objects should encapsulate any necessary validation logic at the time of creation. This guarantees that invalid states are not possible. In the example above, we validate that the account name must have at least three characters before creating the AccountName object:
public static fromString(name: string) {if(name.length < 3) {throw new Error('Account name should contain at least 3 characters');}return new AccountName({ value: name });}By placing validation logic inside the factory method (fromString in this case), we ensure that any AccountName object created is always valid.
-
Equality and comparisons
Section titled “Equality and comparisons”Since Value Objects don’t have an identity, they are compared based on the values of their properties. Two Value Objects are considered equal if all their properties are the same. For example, two AccountName objects with the same name would be equal. The ValueObject class implements this equality check for you, ensuring that value comparisons are based on the object’s properties.
const name1 = AccountName.fromString('John Doe');const name2 = AccountName.fromString('John Doe');name1.equals(name2); // trueValue objects of different classes are never equal, even with the same properties, and
equals(null)orequals(undefined)returnsfalse(since 4.0). -
Domain Logic Encapsulation
Section titled “Domain Logic Encapsulation”Although Value Objects are often simple, they can encapsulate domain-specific logic related to their properties. For instance, you might add formatting logic or rules for special cases. However, keep in mind that Value Objects should remain focused and not include complex domain behaviors, which are usually the responsibility of entities or aggregates.
For example, you could add a method for formatting the account name:
public formatName() {return this.props.value.toUpperCase();} -
Keeping Value Objects Lightweight
Section titled “Keeping Value Objects Lightweight”A Value Object should focus only on encapsulating simple domain attributes and logic. Don’t overload it with responsibilities outside of representing and validating its value.
MIT 2026 © Ocoda.