# Entity Mapping Guide Design §10-§13. The rules here exist because each one has a failure mode that is invisible in review and expensive in production. ## The domain owns the model The platform defines no business entity. Table names, column semantics, keys, unique and check requirements, associations, cascade rules, lock policy, and soft-delete policy all belong to the domain module. There is no `GenericRepository` and no platform base repository, because a single generic API forces every aggregate through the same operations — and one aggregate's later requirement then changes behaviour for all of them. ## Entities must be proxyable - Not `final`. Hibernate creates a lazy proxy by generating a subclass; a final entity cannot be subclassed, so *every* association to it loads eagerly whatever the mapping says. Nothing errors. - A non-private no-arg constructor. The provider instantiates entities reflectively before populating fields. `EntityMappingCondition` in the testkit enforces both. ## Identifiers Default to a sequence with an `allocationSize` that matches the migration's `INCREMENT BY`. When they disagree, the provider hands out identifiers the sequence has not reserved and the collision surfaces later as a primary-key violation under load. `GenerationType.IDENTITY` is supported and limited: the key is assigned on insert, so the provider must execute each insert immediately to learn it, which disables JDBC insert batching entirely. `HibernateBatchConfigurationGuard` fails a batch profile that targets an IDENTITY entity rather than letting the import silently run an order of magnitude slower. UUIDv7 (`UuidV7Generator`) is the application-side option. It is preferred over UUIDv4 for a primary key because v4 is uniformly random: every insert lands on a random leaf of the B-tree, so the index never stays in cache and write amplification grows with the table. ## Values - Enums are `EnumType.STRING` or an explicit converter. **Never** `ORDINAL` — it stores the constant's position, so inserting a new constant anywhere but the end silently reinterprets every existing row. - Money is `BigDecimal` with explicit precision and scale. `double` cannot represent `0.1`, so sums drift and reconciliation disagrees with the ledger. - `Duration` goes through a converter that stores milliseconds. The ISO-8601 text form sorts and compares wrongly in SQL. - `Instant` and `OffsetDateTime` map differently; a column typed for one cannot faithfully store the other. ## Associations - To-one associations are `LAZY`. JPA's default is `EAGER`, which means every query that loads a child also queries for its parent — the most common accidental N+1 in a JPA application. - The owning side holds the foreign key. Adding to the inverse collection alone leaves the row unlinked, so aggregates expose an association helper that sets both sides. - `CascadeType.ALL` with `orphanRemoval` is correct only for a child the aggregate genuinely owns. Between independent aggregates it deletes rows another part of the system still owns. ## Entities never leave the transaction A controller must not return an entity, or a collection or `Optional` of one. Response serialisation happens after the transaction closes, so a lazy association touched by the serialiser either throws or — with OSIV on, which this platform forbids — issues a query from the view layer, one per element. `EntityExposureCondition` checks generic type arguments, not just the erased return type.