{"id":28343184,"url":"https://github.com/ehmpathy/domain-objects","last_synced_at":"2026-04-10T12:06:38.414Z","repository":{"id":45380407,"uuid":"290507199","full_name":"ehmpathy/domain-objects","owner":"ehmpathy","description":"A simple, convenient way to represent domain objects, leverage domain knowledge, and add runtime validation in your code base.","archived":false,"fork":false,"pushed_at":"2025-01-08T09:11:06.000Z","size":4531,"stargazers_count":4,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-03T16:19:43.495Z","etag":null,"topics":["domain-driven-design","modeling","runtime-typechecking","validation"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ehmpathy.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-08-26T13:40:47.000Z","updated_at":"2025-01-08T09:10:03.000Z","dependencies_parsed_at":"2024-05-16T11:44:30.550Z","dependency_job_id":"81873b88-006e-4869-8e6c-f4f3768b2b04","html_url":"https://github.com/ehmpathy/domain-objects","commit_stats":{"total_commits":75,"total_committers":2,"mean_commits":37.5,"dds":0.1333333333333333,"last_synced_commit":"5df051204a4ec5ab15422e8c7fd3cc6a6b54e8a5"},"previous_names":[],"tags_count":56,"template":false,"template_full_name":null,"purl":"pkg:github/ehmpathy/domain-objects","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehmpathy%2Fdomain-objects","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehmpathy%2Fdomain-objects/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehmpathy%2Fdomain-objects/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehmpathy%2Fdomain-objects/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ehmpathy","download_url":"https://codeload.github.com/ehmpathy/domain-objects/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehmpathy%2Fdomain-objects/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260816103,"owners_count":23067223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["domain-driven-design","modeling","runtime-typechecking","validation"],"created_at":"2025-05-27T06:30:50.917Z","updated_at":"2026-04-10T12:06:38.408Z","avatar_url":"https://github.com/ehmpathy.png","language":"TypeScript","readme":"# domain-objects\n\n![test](https://github.com/ehmpathy/domain-objects/workflows/test/badge.svg)\n![publish](https://github.com/ehmpathy/domain-objects/workflows/publish/badge.svg)\n\nA simple, convenient way to represent domain objects, leverage domain knowledge, and add runtime validation in your code base.\n\nGuided by [Domain Driven Design](https://dddcommunity.org/learning-ddd/what_is_ddd/)\n\n# Purpose\n\n- promote speaking in a domain driven manner, in code and in speech, by formally defining domain objects\n- to make software safer and easier to debug, by supporting run time type checking\n- to leverage domain knowledge in your code base\n  - e.g., in comparisons of objects\n  - e.g., in schema based runtime validation\n\n# Install\n\n```sh\nnpm install --save domain-objects\n```\n\n# Usage Examples\n\n### literal\n\n```ts\nimport { DomainLiteral } from 'domain-objects';\n\n// define it\ninterface Address {\n  street: string;\n  suite: string | null;\n  city: string;\n  state: string;\n  postal: string;\n}\nclass Address extends DomainLiteral\u003cAddress\u003e implements Address {}\n\n// use it\nconst austin = new Address({\n  street: '123 South Congress',\n  suite: null,\n  city: 'Austin',\n  state: 'Texas',\n  postal: '78704',\n});\n```\n\n### entity\n\n```ts\nimport { DomainEntity } from 'domain-objects';\n\n// define it\ninterface RocketShip {\n  uuid?: string;\n  serialNumber: string;\n  fuelQuantity: number;\n  passengers: number;\n  homeAddress: Address;\n}\nclass RocketShip extends DomainEntity\u003cRocketShip\u003e implements RocketShip {\n  public static unique = ['serialNumber'];\n  public static updatable = ['fuelQuantity', 'homeAddress'];\n}\n\n// use it\nconst ship = new RocketShip({\n  serialNumber: 'SN5',\n  fuelQuantity: 9001,\n  passengers: 21,\n  homeAddress: new Address({ ... }),\n});\n```\n\n\n### event\n\n```ts\nimport { DomainEvent } from 'domain-objects';\n\n// define it\ninterface AirQualityMeasuredEvent {\n  locationUuid: string;\n  sensorUuid: string;\n  occurredAt: string;\n  temperature: string;\n  humidity: string;\n  pressure: string;\n  pm2p5: string; // PM2.5 : fine inhalable particles, with diameters that are generally 2.5 micrometers\n  pm5p0: string; // PM5.0\n  pm10p0: string; // PM10.0\n}\nclass AirQualityMeasuredEvent extends DomainEvent\u003cAirQualityMeasuredEvent\u003e implements AirQualityMeasuredEvent {\n  public static unique = ['locationUuid', 'sensorUuid', 'occurredAt'];\n}\n\n// use it\nconst event = new AirQualityMeasuredEvent({\n  locationUuid: '8e34eb9b-2874-43e0-bc89-73a73d50ac5c',\n  sensorUuid: 'a17f7941-1211-44f4-a22a-b61f220527da',\n  occurredAt: '2021-07-08T11:13:38.780Z',\n  temperature: '31.52°C',\n  humidity: '27%rh',\n  pressure: '29.99bar',\n  pm2p5: '9ug/m3',\n  pm5p0: '11ug/m3',\n  pm10p0: '17ug/m3',\n});\n```\n\n### runtime validation\n\n\u003e everyone has types until they get punched in the runtime - mike typeson 🥊\n\n```ts\n// define your domain object with a schema this time\ninterface Address {\n  id?: number;\n  galaxy: string;\n  solarSystem: string;\n  planet: string;\n  continent: string;\n}\nconst schema = Joi.object().keys({\n  id: Joi.number().optional(),\n  galaxy: Joi.string().valid(['Milky Way', 'Andromeda']).required(),\n  solarSystem: Joi.string().required(),\n  planet: Joi.string().required(),\n  continent: Joi.string().required(),\n});\nclass Address extends DomainLiteral\u003cAddress\u003e implements Address {\n  public static schema = schema; // supports Zod, Yup, and Joi\n}\n\n// and now when you instantiate objects, the props you instantiate with will be runtime validated\nconst northAmerica = new Address({\n  galaxy: 'Milky Way',\n  solarSystem: 'Sun',\n  planet: 'Earth',\n  continent: 'North America',\n}); // passes, no error\n\nconst westDolphia = new Address({\n  galaxy: 'AndromedA', // oops, accidentally capitalized the last A in Andromeda - this will fail the enum check!\n  solarSystem: 'Asparagia',\n  planet: 'Dracena',\n  continent: 'West Dolphia',\n}); // throws a helpful error, see the `Features` section below for details\n```\n\n### identity comparison\n\n```ts\nimport { serialize, getUniqueIdentifier } from 'domain-objects';\n\nconst northAmerica = new Address({\n  galaxy: 'Milky Way',\n  solarSystem: 'Sun',\n  planet: 'Earth',\n  continent: 'North America',\n});\nconst northAmericaWithId = new Address({\n  id: 821, // we pulled this record from the db, so it has an id\n  galaxy: 'Milky Way',\n  solarSystem: 'Sun',\n  planet: 'Earth',\n  continent: 'North America',\n});\n\n// is `northAmerica` the same object as `northAmericaWithId`?\nconst areTheSame = serialize(getUniqueIdentifier(northAmerica)) === serialize(getUniqueIdentifier(northAmericaWithId)); // because of domain modeling, we know definitively that this is `true`!\n```\n\n### change detection\n\n```ts\nimport { serialize, omitMetadata } from 'domain-objects';\n\n// shiny new spaceship, full of fuel\nconst sn5 = new Spaceship({\n  serialNumber: 'SN5',\n  fuelQuantity: 9001,\n  passengers: 21,\n});\n\n// lets save it to the database\nconst sn5Saved = new Spaceship({ ...sn5, id: 821, updatedAt: now() }); // the database will add metadata to it\n\n// lets check that in the process of saving to the database, no unexpected changes were introduced\nconst hadChangeDuringSave = serialize(omitMetadata(sn5)) !== serialize(omitMetadata(sn5Saved)); // note: we omit the metadata values since we dont care that one has db generated values like id specified and the other does not\nexpect(hadChangeDuringSave).toEqual(false); // even though an id was added to sn5Saved, the non-metadata attributes have not changed, so we can say there is no change as desired\n\n// we do some business logic, and in the process, the space ship flys around and uses up fuel\nconst sn5AfterFlying = new Spaceship({ ...sn5, fuelQuantity: 4500 });\n\n// lets programmatically detect whether there was a change now\nconst hadChangeAfterFlying = serialize(omitMetadata(spaceport)) !== serialize(omitMetadata(spaceportAfterFlight));\nexpect(hadChangeAfterFlying).toEqual(true); // because the fuelQuantity has decreased, the Spaceship has had a change after flying\n```\n\n# Features\n\n## Declaration\n\nModel declaration is a fundamental part of domain driven design. Here is how you can declare your model in your code - to aid in building a ubiquitous language.\n\n### `DomainLiteral`\n\nIn Domain Driven Design, a Literal (a.k.a. Value Object), is a type of Domain Object for which:\n\n - properties are immutable\n   - i.e., it represents some literal value which happens to have a structured object shape\n   - i.e., if you change the value of any of its properties, it is a different literal\n - identity does not matter\n   - i.e., it is uniquely identifiable by its non-metadata properties\n\n```ts\n// define it\ninterface Address {\n  street: string;\n  suite: string | null;\n  city: string;\n  state: string;\n  postal: string;\n}\nclass Address extends DomainLiteral\u003cAddress\u003e implements Address {}\n\n// use it\nconst austin = new Address({\n  street: '123 South Congress',\n  suite: null,\n  city: 'Austin',\n  state: 'Texas',\n  postal: '78704',\n});\n```\n\n### `DomainEntity`\n\nIn Domain Driven Design, an Entity is a type of Domain Object for which:\n\n- properties change over time\n  - e.g., it has a lifecycle\n- identity matters\n  - i.e., it represents a distinct existence\n  - e.g., two entities could have the same properties, differing only by id, and are still considered different entities\n  - e.g., you can update properties on an entity and it is still considered the same entity\n\n```ts\n// define it\ninterface RocketShip {\n  uuid?: string;\n  serialNumber: string;\n  fuelQuantity: number;\n  passengers: number;\n  homeAddress: Address;\n}\nclass RocketShip extends DomainEntity\u003cRocketShip\u003e implements RocketShip {\n  /**\n   * an entity is uniquely identifiable by some subset of their properties\n   *\n   * in order to use the `getUniqueIdentifier` and `serialize` methods on domain entities,\n   * we must define the properties that the entity is uniquely identifiable by.\n   */\n  public static unique = ['serialNumber'];\n}\n\n// use it\nconst ship = new RocketShip({\n  serialNumber: 'SN5,\n  fuelQuantity: 9001,\n  passengers: 21,\n  homeAddress: new Address({ ... }),\n});\n```\n\n## References (`Ref`, `RefByUnique`, `RefByPrimary`)\n\nIn work with entities and events, you often need to refer to them by their **primary key** (e.g., `uuid`) or by their **unique keys** (e.g., a compound unique such as `{ source, exid }`). `domain-objects` provides utility types to make this type-safe.\n\n### `RefByPrimary\u003ctypeof DomainObject\u003e`\n\nRefByPrimary extracts the shape of the primary key for a given domain object.\n\n```ts\nimport { DomainEntity, RefByPrimary } from 'domain-objects';\n\ninterface SeaTurtle {\n  uuid?: string;\n  seawaterSecurityNumber: string;\n  name: string;\n}\nclass SeaTurtle extends DomainEntity\u003cSeaTurtle\u003e implements SeaTurtle {\n  public static primary = ['uuid'] as const;\n  public static unique = ['seawaterSecurityNumber'] as const;\n}\n\n// ✅ valid\nconst primaryRef: RefByPrimary\u003ctypeof SeaTurtle\u003e = { uuid: 'beefbeef...' };\n\n// ❌ invalid - must be a string\nconst wrongType: RefByPrimary\u003ctypeof SeaTurtle\u003e = { uuid: 8335 };\n\n// ❌ invalid - wrong key\nconst wrongKey: RefByPrimary\u003ctypeof SeaTurtle\u003e = { guid: 'beefbeef...' };\n\n// ❌ invalid - missing primary key\nconst missing: RefByPrimary\u003ctypeof SeaTurtle\u003e = {};\n```\n\n### `RefByUnique\u003ctypeof DomainObject\u003e`\n\nRefByUnique extracts the shape of the unique key(s) for a given domain object.\n\n```ts\nimport { DomainEntity, RefByUnique } from 'domain-objects';\n\ninterface SeaTurtle {\n  uuid?: string;\n  seawaterSecurityNumber: string;\n  name: string;\n}\nclass SeaTurtle extends DomainEntity\u003cSeaTurtle\u003e implements SeaTurtle {\n  public static primary = ['uuid'] as const;\n  public static unique = ['seawaterSecurityNumber'] as const;\n}\n\n// ✅ valid\nconst uniqueRef: RefByUnique\u003ctypeof SeaTurtle\u003e = { seawaterSecurityNumber: 'ABC-999' };\n\n// ❌ invalid - wrong type\nconst wrongType: RefByUnique\u003ctypeof SeaTurtle\u003e = { seawaterSecurityNumber: 999 };\n\n// ❌ invalid - wrong key\nconst wrongKey: RefByUnique\u003ctypeof SeaTurtle\u003e = { saltwaterSecurityNumber: 'ABC-999' };\n\n// ❌ invalid - empty object\nconst empty: RefByUnique\u003ctypeof SeaTurtle\u003e = {};\n```\n\n### `Ref\u003ctypeof DomainObject\u003e`\n\nRef is a union type that allows referring to a domain object by either primary key or unique keys.\n\n```ts\nimport { DomainEntity, Ref } from 'domain-objects';\n\ninterface EarthWorm {\n  uuid?: string;\n  soilSecurityNumber: string;\n  wormSegmentNumber: string;\n  name: string;\n}\nclass EarthWorm extends DomainEntity\u003cEarthWorm\u003e implements EarthWorm {\n  public static primary = ['uuid'] as const;\n  public static unique = ['soilSecurityNumber', 'wormSegmentNumber'] as const;\n}\n\n// ✅ primary\nconst byPrimary: Ref\u003ctypeof EarthWorm\u003e = { uuid: 'beefbeef...' };\n\n// ✅ unique\nconst byUnique: Ref\u003ctypeof EarthWorm\u003e = {\n  soilSecurityNumber: 'SOIL-001',\n  wormSegmentNumber: 'SEG-42',\n};\n\n// ❌ invalid - missed part of unique key\nconst incompleteUnique: Ref\u003ctypeof EarthWorm\u003e = { soilSecurityNumber: 'SOIL-001' };\n\n// ❌ invalid - not related to either key\nconst wrongKey: Ref\u003ctypeof EarthWorm\u003e = { guid: 'beefbeef...' };\n\n// ❌ invalid - empty object\nconst empty: Ref\u003ctypeof EarthWorm\u003e = {};\n```\n\n👉 Use `RefByPrimary` for primary-only references,\n👉 `RefByUnique` for unique-only references,\n👉 `Ref` when you want to allow either.\n\n### Instantiating Reference Objects\n\nYou can instantiate reference objects directly using the `RefByUnique` or `RefByPrimary` constructors:\n\n```ts\nimport { RefByUnique, RefByPrimary } from 'domain-objects';\n\n// Using RefByUnique\nconst turtleRef = RefByUnique.build\u003ctypeof SeaTurtle\u003e({\n  seawaterSecurityNumber: '821',\n});\n\n// Using RefByPrimary\nconst turtleRefById = RefByPrimary.build\u003ctypeof SeaTurtle\u003e({\n  uuid: 'beefbeef-cafe-babe-0000-000000000001',\n});\n```\n\n### Nested Reference Hydration\n\nJust like other nested domain objects, references can be automatically hydrated when used as nested properties:\n\n```ts\nimport { DomainEntity, RefByUnique, RefByPrimary } from 'domain-objects';\n\ninterface SeaTurtleShell {\n  turtle: RefByUnique\u003ctypeof SeaTurtle\u003e;\n  algea: 'ALOT' | 'ALIL';\n}\nclass SeaTurtleShell extends DomainEntity\u003cSeaTurtleShell\u003e implements SeaTurtleShell {\n  public static unique = ['turtle'] as const;\n  public static nested = {\n    turtle: RefByUnique\u003ctypeof SeaTurtle\u003e,\n  };\n}\n\n// now you can pass a plain object and it will be hydrated as a RefByUnique\nconst shell = new SeaTurtleShell({\n  turtle: { seawaterSecurityNumber: '821' }, // plain object\n  algea: 'ALOT',\n});\n\nexpect(shell.turtle).toBeInstanceOf(RefByUnique); // ✅ automatically hydrated!\nexpect(shell.turtle.seawaterSecurityNumber).toEqual('821');\n```\n\n## Run Time Validation\n\nRuntime validation is a great way to fail fast and prevent unexpected errors.\n\n`domain-objects` supports an easy way to add runtime validation, by defining a [`Zod`](https://github.com/colinhacks/zod), [`Yup`](https://github.com/jquense/yup), or [`Joi`](https://github.com/sideway/joi) schema.\n\nWhen you provide a schema in your type definition, your domain objects will now be run time validated at instantiation.\n\nexample:\n\n```ts\n// with this declaration of a \"RocketShip\", the schema specifies that there can be a max of 42 passengers\ninterface RocketShip {\n  serialNumber: string;\n  fuelQuantity: number;\n  passengers: number;\n}\nconst schema = Joi.object().keys({\n  serialNumber: Joi.string().uuid().required(),\n  fuelQuantity: Joi.number().required(),\n  passengers: Joi.number().max(42).required(),\n});\nclass RocketShip extends DomainObject\u003cRocketShip\u003e implements RocketShip {\n  public static schema = schema;\n}\n\n// so if we try the following, we will get an error\nnew RocketShip({\n  serialNumber: uuid(),\n  fuelQuantity: 9001,\n  passengers: 50,\n});\n\n// throws JoiValidationError\n```\n\nWe made sure that the errors are as descriptive as possible to help with debugging. For example, the error that would have been shown above has the following message:\n\n```\nErrors on 1 properties were found while validating properties for domain object RocketShip.:\n[\n  {\n    \"message\": \"\\\"passengers\\\" must be less than or equal to 42\",\n    \"path\": \"passengers\",\n    \"type\": \"number.max\"\n  }\n]\n\nProps Provided:\n{\n  \"serialNumber\": \"eeb6988c-d877-4268-b841-bde2f40b377e\",\n  \"fuelQuantity\": 9001,\n  \"passengers\": 50\n}\n```\n\n## Nested Hydration\n\n\u003e _TL:DR;_ Without `DomainObject.nested`, you will need to manually instantiate nested domain objects every time. If you forget, `getUniqueIdentifier` and `serialize` will throw errors.\n\nNested hydration is useful when instantiating DomainObjects that are composed of other DomainObjects. For example, in the `RocketShip` example above, `RocketShip` has `Address` as a nested property (i.e., `typeof Spaceship.address === Address`).\n\nWhen attempting to manipulate DomainObjects with nested DomainObjects, like the Spaceship.address example, it is important that all nested domain objects are instantiated with their class. Otherwise, if `RocketShip.address` is not an instanceof `Address`, then we will not be able to utilize the domain information baked into the static properties of `Address` (e.g., that it is a DomainLiteral).\n\n`domain-objects` makes it easy to instantiate nested DomainObjects, by exposing the `DomainObject.nested` static property.\n\nFor example:\n\n```ts\n// define the domain objects that you'll be nesting\ninterface PlantPot {\n  diameterInInches: number;\n}\nclass PlantPot extends DomainLiteral\u003cPlantPot\u003e implements PlantPot {}\ninterface PlantOwner {\n  name: string;\n}\nclass PlantOwner extends DomainEntity\u003cPlantOwner\u003e implements PlantOwner {}\n\n// define the plant\ninterface Plant {\n  pot: PlantPot;\n  owners: PlantOwner[];\n  lastWatered: string;\n}\nclass Plant extends DomainEntity\u003cPlant\u003e implements Plant {\n  /**\n   * define that `pot` and `owners` are nested domain objects, and specify which domain objects they are, so that they can be hydrated during instantiation if needed.\n   */\n  public static nested = { pot: PlantPot, owners: PlantOwner };\n}\n\n// instantiate your domain object\nconst plant = new Plant({\n  pot: { diameterInInches: 7 }, // note, not an instance of `PlantPot`\n  owners: [{ name: 'bob' }], // note, not an instance of `PlantOwner`\n  lastWatered: 'monday',\n});\n\n// and find that, because `.nested.pot` was defined, `pot` was instantiated as a `PlantPot`\nexpect(plant.pot).toBeInstanceOf(PlantPot);\n\n// and find that, because `.nested.owners` was defined, each element of `owners` was instantiated as a `PlantOwner`\nplant.owners.forEach((owner) =\u003e expect(owner).toBeInstance(PlantOwner));\n```\n\nYou may be thinking to yourself, \"Didn't i just define what the nested DomainObjects were in the type definition, when defining the interface? Why do i have to define it again?\". Agreed! Unfortunately, typescript removes all type information at runtime. Therefore, we have no choice but to repeat this information in another way if we want to use this information at runtime. (See #8 for progress on automating this).\n\n## fn `getUniqueIdentifier(obj: DomainEntity | DomainLiteral)`\n\nDomain models inform us of what properties uniquely identify a domain object.\n\ni.e.,:\n- literals are uniquely identified by all of their non-metadata properties\n- entities are uniquely identified by an explicitly subset of their properties, declared via the `.unique` static property\n\nthis `getUniqueIdentifier` function leverages this knowledge to return a normal object containing only the properties that uniquely identify the domain object you give it.\n\n## fn `serialize(value: any)`\n\nDomain modeling gives additional information that we can use for `change detection` and `identity comparisons`.\n\n`domain-objects` allows us to use that information conveniently with the functions `serialize`.\n\n`serialize` deterministically converts any object you give it into a string representation:\n\n- deterministically sort all array items\n- deterministically sort all object keys\n- remove non-unique properties from nested domain objects\n\ndue to this deterministic serialization, we are able to use this fn for [`change detection`](#change-detection) and [`identity comparisons`](#identity-comparison). See the [examples](#usage-examples) section above for an example of each.\n\n\n## Readonly vs Metadata Properties\n\nDomain objects support two categories of readonly properties. Both are set by the persistence layer, but they differ in what they describe. **Metadata is a special subset of readonly** - all metadata is readonly, but not all readonly is metadata.\n\n### Metadata Properties (Persistence Descriptors - All Domain Objects)\n\n**Metadata** are attributes set by the persistence layer that describe the persistence of the object - not intrinsic attributes of the domain object itself. This is the most common type of readonly property and applies to **all domain objects** (entities, events, and literals).\n\n- Default metadata keys: `id`, `uuid`, `createdAt`, `updatedAt`, `effectiveAt`\n- Customize via `static metadata = ['...'] as const;`\n- Omit with `omitMetadata(obj)`\n\n```ts\nclass User extends DomainEntity\u003cUser\u003e implements User {\n  public static primary = ['id'] as const;\n  public static unique = ['email'] as const;\n  public static metadata = ['id', 'createdAt', 'updatedAt'] as const;\n}\n```\n\n### Readonly Properties (Intrinsic Attributes Set by Persistence - Entities Only)\n\n**Readonly** (non-metadata) are intrinsic attributes of the object that the persistence layer sets. Unlike metadata (which describes the persistence), these describe real attributes of the domain object.\n\n- Only applicable to **DomainEntity** (not DomainEvent or DomainLiteral)\n- No default readonly keys (domain-specific, must be explicitly declared)\n- Declare via `static readonly = ['...'] as const;`\n- Omit with `omitReadonly(obj)` - this omits **both** metadata AND explicit readonly keys\n\n**Why only DomainEntity?**\n- **DomainEvent**: Immutable by nature. All properties are known before persistence - there's no concept of persistence-layer-set intrinsic attributes.\n- **DomainLiteral**: Immutable by nature and fully defined by intrinsic properties. If a property changes, it's a different literal.\n\n```ts\nclass AwsRdsCluster extends DomainEntity\u003cAwsRdsCluster\u003e implements AwsRdsCluster {\n  public static primary = ['arn'] as const;\n  public static unique = ['name'] as const;\n  public static metadata = ['arn'] as const;                    // AWS-assigned identity (describes persistence)\n  public static readonly = ['host', 'port', 'status'] as const; // AWS-resolved intrinsic attributes (describes the object)\n}\n```\n\n### Key Distinction\n\n| Aspect | Metadata | Readonly (broader) |\n|--------|----------|----------|\n| Relationship | A special **subset of** readonly | The **superset** containing metadata + more |\n| Applies to | All domain objects | DomainEntity only |\n| What it describes | Persistence of the object | Intrinsic attributes of the object |\n| Set by | Persistence layer | Persistence layer |\n| Default keys | Yes (`id`, `uuid`, etc.) | No (explicit only) |\n| Omit function | `omitMetadata()` | `omitReadonly()` (includes metadata) |\n\n\n## `DomainObject.build`\n\nAdd getters to your domain object instances, easily.\n\nBy default, .build will wrap your dobj instances `withImmute`, to give you immute operations such as `.clone(andSet?: Partial\u003cT\u003e)`\n\nFor example,\n```ts\nconst ship = RocketShip.build({\n  serialNumber: 'SN1',\n  fuelQuantity: 9001,\n  passengers: 3,\n});\nconst shipTwin = ship.clone()\nconst shipUsed = ship.clone({ fuelQuantity: 821 })\n```\n\nNote, you can override your DomainObject's .build procedure to add your own getters\n\nFor example,\n```ts\n\n```\n\nThis gives you a simple way to enrich your objects with domain-specific logic, while still preserving immutability and ergonomics.\n\n## `withImmute`\n\n\nWraps any domain object to make it safer to use via immutable operations.\n\nImmutability helps avoid bugs caused by shared object references - where multiple procedures unintentionally share the same instance of data in memory. This is especially common concern in systems which leverage parallelism.\n\n`withImmute` adds immute operators to your dobj, such as\n- `.clone(update?: Partial\u003cT\u003e)`\n\nAdded by default via `.build()`. Available for adhoc usage too:\n\n```ts\nconst plant = withImmute(new Plant({ ... }));\nconst twin = plant.clone()\n```\n\n## `withImmute.clone(update?: Partial\u003cT\u003e)`\n\nCreates a new instance of the domain object with updated values — without modifying the original.\n\nThis is helpful when working in a system that depends on immutability, such as functional logic, undo/redo flows, or parallel processing, where unintended mutations can introduce bugs.\n\nThe `.clone()` method uses deep cloning and deep merging:\n- Every nested value is safely copied.\n- Only the fields you provide in the `update` are changed.\n- Original object remains untouched.\n\nExample:\n\n```ts\nconst plant = Plant.build({\n  plantedIn: new PlantPot({ diameterInInches: 5 }),\n  lastWatered: 'Monday',\n});\n\nconst updated = plant.clone({ lastWatered: 'Tuesday' });\n\nexpect(updated.lastWatered).toEqual('Tuesday');\nexpect(plant.lastWatered).toEqual('Monday'); // original is unchanged\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fehmpathy%2Fdomain-objects","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fehmpathy%2Fdomain-objects","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fehmpathy%2Fdomain-objects/lists"}