skip to content

At architectural scale, how does the GRASP "Creator" heuristic extend into lifecycle ownership — who may create objects that cross module or bounded-context boundaries, and how does object reconstitution by persistence frameworks interact with it?

level: principalimportance: nice to knowfreq 18%

answer

  1. creation = first enforcement of an invariant
  2. only the aggregate root creates its parts
  3. foreign modules send commands/data, owner constructs
  4. ACL is the local Creator at context edges
  5. reconstitution ≠ creation: no policies re-run

basics

~20 s

Each module should own creation of its own types: outsiders send a request or data, and the owning module builds the object and checks the rules. Loading objects from a database is rebuilding an already-valid object, not creating a new one, so it may use a separate hidden path.

solid answer

~60 s

Scaled up, Creator becomes a rule about **who may bring a valid instance into existence**. Within an aggregate, only the root creates its parts, so invariants have a single enforcement point. Across modules or bounded contexts, a foreign module must never construct another context's entity: it publishes a command, event, or translated data, and the owning module constructs and validates. Otherwise validation rules leak outward, constructors become de-facto public API, and the owner cannot evolve its invariants without breaking callers. Practical mechanisms: package-private/internal constructors plus a published creation method on the module's public API; anti-corruption layers translating foreign representations into local types; and factories owned by the module, never by the caller. **Reconstitution** — an ORM, deserializer, or event-sourcing replay rebuilding an object from stored state — is deliberately distinct from creation: it must not re-run creation-time policies (id assignment, welcome events, current-price snapshots) because the object was already validated once. Keep a separate, restricted reconstitution path, and be explicit that migrations, not constructors, fix historical data that violates today's rules.

go deeper

for a junior

Say that the class owning the object should create it, and that loading from a database is not the same as creating something new.

for a middle

Add the aggregate-root rule and the idea that other modules should ask the owner to create rather than calling the constructor themselves.

for a senior

Explain constructor visibility, published creation methods, anti-corruption layers, and the concrete bugs from conflating reconstitution with creation.

for a principal

Frame creation as the invariant-enforcement boundary; cover module/context ownership, enforcement via architecture tests, id-generation and idempotency trade-offs across the network, event-sourcing replay purity, wire-contract versioning, and the shared-kernel failure mode.

## From class-level heuristic to architectural policy The classic **GRASP Creator** rule (create `A` in the class that aggregates, contains, records, closely uses `A`, or holds its initializing data) is stated for classes. The same reasoning generalises: **creation is the moment when an invariant is first established**, so whoever is accountable for the invariant must control creation. Scaling that idea gives three concentric rules. ### Level 1 — Inside an aggregate An **aggregate** is a cluster of objects treated as one consistency unit, with a single **root** as its only external entry point. Rule: **only the root creates its parts.** Parts have no independently addressable existence, so external construction would let someone build a part that never satisfies the root's cross-part rules (totals, ordering, uniqueness, state gating). Enforcement is usually visibility: parts get non-public constructors within the module. ### Level 2 — Across modules in one process Each module owns a set of types. Rule: **a module never constructs another module's types directly.** It calls the owning module's published API with plain data or a command, and the owner constructs. Consequences if violated: - Constructors become the *de facto* public contract; the owner can no longer add a required field or a rule without breaking callers. - Validation logic gets duplicated — often subtly differently — at every foreign construction site. - Dependency direction inverts silently: the caller now depends on the owner's internal shape, not on its interface. Mechanisms: `internal`/package-private constructors; a published `create*`/`register*` method on the module API; the module's own factory; architecture tests (e.g. module-boundary verification tooling) that fail the build when a foreign package instantiates an owned type. ### Level 3 — Across bounded contexts A **bounded context** is a boundary within which a model and its terms are consistent. The same word means different things across contexts (a "Customer" in Billing ≠ a "Customer" in Support). Rule: **never construct another context's model objects.** Communication goes through **published contracts** — events, commands, API payloads — and an **anti-corruption layer (ACL)** translates the foreign representation into the local model, applying local validation. The ACL *is* the local Creator: it holds the incoming data (criterion 5) and produces a locally valid instance. ## Creation vs reconstitution — the essential distinction | | **Creation** | **Reconstitution** | |---|---|---| | Meaning | a new object begins to exist | an existing, already-valid object is rebuilt from stored state | | Triggered by | a domain command | a database load, deserialization, cache read, event replay | | Must run | id assignment, defaulting, policy checks, price snapshots, creation events | none of those | | Validation | full current rules | structural only; historical data may violate today's rules | | Access | public creation method on the owner | restricted path (framework/package-private constructor, static `rehydrate`) | Getting this wrong produces classic bugs: - Loading an order re-emits `OrderPlaced` (event storm, duplicate side effects). - Loading re-snapshots the *current* product price, silently rewriting historical totals. - Tightening a validation rule makes old rows unloadable — the system cannot even read its own history to migrate it. **Migrations fix history; constructors must not.** - In **event sourcing**, replay must be pure: applying stored events rebuilds state without re-running the decision logic (the `decide` step) that produced them. So: keep two paths, name them differently (`Order.place(...)` vs `Order.rehydrate(...)`), and restrict visibility of the second so application code cannot use it to dodge invariants. ## Other boundary-crossing creation concerns at scale - **Identity generation.** Who mints ids — the client (UUID, enables idempotency and offline creation), the owning service, or the database (sequence)? This is a creation-ownership decision with distributed-systems consequences: client-generated ids make retries idempotent; database-generated ids force a round-trip before the object has identity. - **Idempotency.** Creation across a network can be delivered twice. The owning module needs a dedup key or natural key so the second delivery does not create a second instance. Ownership of creation is also ownership of the dedup rule. - **Referential creation across contexts.** Context B receiving `CustomerRegistered` from A typically creates a *local* lightweight projection, not a copy of A's entity. It owns that projection; A owns the real thing. - **Serialization contracts and versioning.** Once a type crosses a boundary, its wire form is a contract with independent lifetime from the class. Creation from an older version must be tolerated (defaults for new fields, ignore unknown fields). - **Testability.** A module that owns creation can offer test builders; foreign modules constructing internals means every test suite couples to internal shape. ## Failure modes to name in review 1. **Constructor as public API** — external packages instantiating owned types; invariants unversionable. 2. **Shared kernel creep** — a common "model" module everyone constructs from, so no one owns any rule; every change is a coordinated release. 3. **Reconstitution masquerading as creation** — ORM entry points running creation-time policies. 4. **Anemic ACL** — a translator that copies fields without validating, so foreign invalid data enters the local model. 5. **Two creation paths that drift** — an import path and a UI path with different rules; the divergence surfaces months later as data no downstream consumer can process. ## The compressed statement > Creation is the enforcement point of an invariant. Push creation to the smallest scope that is accountable for the invariant — root within aggregate, module within process, context within system — and give reconstitution its own restricted path so replaying the past is never confused with authoring the future.

  • How do you technically prevent another module from constructing your types?
    Restrict constructor visibility (internal/package-private), publish a creation method on the module's API, and enforce it with architecture tests or module-boundary verification that fails the build when a foreign package instantiates an owned type. Code review alone does not hold over time.
  • Why must reconstitution skip creation-time policies?
    Because the object was already validated when it was first created, and stored state may legitimately predate today's rules. Re-running policies would re-emit domain events, re-snapshot values such as prices with current data, and make historical rows unloadable after a rule is tightened — blocking the very migration needed to fix them.
  • Who should generate identifiers for objects created across a network boundary?
    Client-generated identifiers (e.g. UUIDs) make retries idempotent and allow offline creation, at the cost of trusting the caller and losing sequential locality. Server- or database-generated ids centralize control but require a round-trip before the object has identity and complicate deduplication of retried requests, which then needs a separate idempotency key.

context