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?
answer
- creation = first enforcement of an invariant
- only the aggregate root creates its parts
- foreign modules send commands/data, owner constructs
- ACL is the local Creator at context edges
- reconstitution ≠ creation: no policies re-run
basics
~20 sEach 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 sScaled 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
Say that the class owning the object should create it, and that loading from a database is not the same as creating something new.
Add the aggregate-root rule and the idea that other modules should ask the owner to create rather than calling the constructor themselves.
Explain constructor visibility, published creation methods, anti-corruption layers, and the concrete bugs from conflating reconstitution with creation.
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.