Inheritance Mapping Strategies
Purpose
Choose a subtype mapping with its query cost, its constraint capability and its evolution
cost all on the table, and challenge the hierarchy itself first. Inheritance is the sharpest
edge of the impedance mismatch: relational schemas have no subtypes, so every strategy is a
compromise, and the compromise is a schema commitment that costs a migration to revisit.
Challenge hierarchies introduced only to share fields, but do not infer that a data-oriented
subtype is invalid: substitutability and distinct invariants matter as well as methods.
Inspect Java release/toolchain, persistence API (javax versus jakarta), provider/version,
dialect, generated SQL and managed migrations first. Guidance is checked against Jakarta
Persistence 3.2; TABLE_PER_CLASS support is optional there. Examples are partial sketches,
not complete entities. Java sealed types require 17, pattern-switch requires 21 without
preview; neither requires upgrading the target project.
The three strategies
Single table one table, all subtypes, a discriminator column.
(SINGLE_TABLE) Subtype-specific columns are commonly nullable;
discriminator-aware CHECK constraints can enforce groups.
Class table one table per class in the hierarchy, joined by the
(JOINED) shared primary key. Subtype entity loads generally join
through mapped levels; projections may not.
Concrete table one table per concrete subtype, each with every
(TABLE_PER_CLASS) column. No shared table, so polymorphic queries are
typically UNIONs or separate queries. An ordinary
FK cannot target the hierarchy without a base table.
Workflow
- Challenge the hierarchy. Do subtypes differ in behaviour, or only in which fields
are populated? Inspect substitutability and invariants; field-only differences invite comparison with
composition but do not alone disprove a subtype.
- Count the shape. How many subtypes, how many columns each, how many are
subtype-specific? Measure populated row width, indexes, constraints and query plans; column count alone
does not determine cost.
- Establish the query mix. Mostly polymorphic reads ("all payments") favour single
table; mostly per-subtype reads with heavy write integrity favour joined.
- Establish whether the database must enforce the subtype's required fields. If yes,
single table is out unless you are prepared to write check constraints.
- Establish the evolution rate. A hierarchy that gains a subtype every quarter pays a
migration per subtype under joined and concrete table, and may require columns, discriminator checks and indexes under single table.
- Decide, and pin the discriminator values explicitly, so a class rename is not a data
migration.
Decision rules
Subtypes only share fields and have no distinct substitution/invariant contract
→ compare composition or mapped-superclass reuse. An enum with embedded
groups may fit; JSON requires explicit validation/query/index trade-offs.
Few subtypes, few subtype-specific columns, polymorphic queries common,
performance matters
→ SINGLE_TABLE often minimizes joins for polymorphic reads. Confirm
row width, indexes, predicates and workload before claiming speed.
Pay with nullable columns and weak database-level constraints.
Many subtype-specific columns, or the database must enforce them, or
subtypes are large and distinct
→ JOINED. Normalised and constrained; measure joins for entity loads and inserts across mapped tables;
base projections need not load every subtype.
Subtypes are genuinely unrelated in storage terms, never queried
polymorphically, and no other table needs a foreign key to the base
→ TABLE_PER_CLASS. Rarely the right answer; an ordinary FK needs a single referenced table;
hierarchy-wide queries usually require UNIONs or separate queries.
The hierarchy is deep (3+ levels)
→ inspect mapped levels and actual fields/SQL before flattening.
Depth can add joins; it does not itself add SINGLE_TABLE columns.
Variation is per-tenant or per-configuration, and new variants must ship
without a deploy
→ not inheritance at all: data-driven variation, with the
variant part in a serialized LOB (orm-structural-mapping).
Rules
- Single table's real cost includes constraint complexity, not just disk. Columns may remain
nullable at the column level while discriminator-aware
CHECK constraints enforce “a
CardPayment must have card_last4.” Verify ORM-generated DDL and bulk/import paths, or
accept that only application code enforces it — and that bulk imports bypass it.
- Joined entity materialization often joins mapped levels/subtypes, while base-only
projections can avoid them; writes span the tables holding the entity state. It is usually acceptable; it becomes painful on a list
screen that reads the whole hierarchy at volume, and that is a measurable question, not a
matter of taste (
architecture-and-performance).
- Concrete table per class has no single base table for an ordinary polymorphic foreign key.
Alternatives such as separate subtype FKs, a registry/base identity table or application-level
integrity add complexity; price them before excluding the strategy.
- Pin stored discriminator values where used (
@DiscriminatorValue("CARD")). For a
STRING discriminator the default is the entity name; an unpinned entity name follows the
class name. Preserve existing values during renames, including explicit entity/table names.
- Query costs depend on selected attributes, predicates, fetches and provider SQL. A root
SINGLE_TABLE query need not filter the discriminator at all; fewer joins do not guarantee
a cheap plan. Inspect plans and representative workload before comparing strategies.
- A hierarchy that only shares fields wants
@MappedSuperclass (shared mapping, no
polymorphism, no base table) or composition. @MappedSuperclass is under-used and is
exactly right for audit columns and shared identifiers.
- Adding a subtype may need new columns/checks/indexes (single table), a new mapped table
and FK (joined), or a concrete table. Locking and scan/rewrite costs depend on the exact
database/version and DDL; additive does not mean online. Changing
strategy later usually requires a substantial data migration. It is expensive, not literally
one-way; use expand/contract, reconciliation and rollback/forward-fix analysis
(
architecture-decision-making).
- Choose indexes from predicates and selectivity. Partial/filtered indexes can help sparse
subtype data where supported and where the query implies their predicate; inspect the
generated, possibly parameterized SQL and plan.
- The domain question comes first. If
PremiumCustomer and StandardCustomer differ only
in a discount rate, they are one type with a policy, and no mapping strategy will make the
subtype earn its keep (domain-logic-organization).
For a recommendation, report query/DDL evidence, the integrity and migration trade-offs,
and the targeted validation that would confirm it. Without plans or workload data, keep
performance conclusions conditional.
References
- Strategy comparison — the three strategies with
their DDL, their generated SQL for the queries that matter, query-cost hypotheses,
constraint enforcement including conditional check constraints, indexing under each, and
the alternatives to inheritance with the shape that replaces it. Read when choosing a
strategy or justifying an existing one.
- Schema evolution — what adding, removing, splitting or
moving a subtype costs under each strategy; discriminator value management and renames;
migrating between strategies with an expand/contract sequence; and the read-model options
when the write-side mapping is right but reads are expensive. Read before changing a
hierarchy that has data in it.
1---2name: inheritance-mapping-strategies3description: Mapping a subtype hierarchy onto tables — single table, class table (joined), concrete table per class — and deciding whether the hierarchy should exist at all. Use when an @Inheritance strategy is being chosen, when a single-table mapping is forcing every subtype's columns to be nullable, when a joined mapping's polymorphic query joins six tables to render a list, when adding a subtype requires a migration, when a discriminator column has drifted from the class names, when polymorphic queries are slow or return the wrong rows, when a hierarchy exists only to share three fields, or when composition would serve better than subtyping. Does not cover associations, identity and value mapping (orm-structural-mapping), where the mapping instructions live (metadata-mapping), the runtime fetch behaviour (orm-behavioral-patterns), or the domain question of whether subtypes model the business correctly (domain-logic-organization).4---56# Inheritance Mapping Strategies78## Purpose910Choose a subtype mapping with its query cost, its constraint capability and its evolution11cost all on the table, and challenge the hierarchy itself first. Inheritance is the sharpest12edge of the impedance mismatch: relational schemas have no subtypes, so every strategy is a13compromise, and the compromise is a schema commitment that costs a migration to revisit.1415Challenge hierarchies introduced only to share fields, but do not infer that a data-oriented16subtype is invalid: substitutability and distinct invariants matter as well as methods.1718Inspect Java release/toolchain, persistence API (`javax` versus `jakarta`), provider/version,19dialect, generated SQL and managed migrations first. Guidance is checked against Jakarta20Persistence 3.2; TABLE_PER_CLASS support is optional there. Examples are partial sketches,21not complete entities. Java sealed types require 17, pattern-switch requires 21 without22preview; neither requires upgrading the target project.2324## The three strategies2526```text27Single table one table, all subtypes, a discriminator column.28(SINGLE_TABLE) Subtype-specific columns are commonly nullable;29 discriminator-aware CHECK constraints can enforce groups.3031Class table one table per class in the hierarchy, joined by the32(JOINED) shared primary key. Subtype entity loads generally join33 through mapped levels; projections may not.3435Concrete table one table per concrete subtype, each with every36(TABLE_PER_CLASS) column. No shared table, so polymorphic queries are37 typically UNIONs or separate queries. An ordinary38 FK cannot target the hierarchy without a base table.39```4041## Workflow42431. **Challenge the hierarchy.** Do subtypes differ in _behaviour_, or only in which fields44 are populated? Inspect substitutability and invariants; field-only differences invite comparison with45 composition but do not alone disprove a subtype.462. **Count the shape.** How many subtypes, how many columns each, how many are47 subtype-specific? Measure populated row width, indexes, constraints and query plans; column count alone48 does not determine cost.493. **Establish the query mix.** Mostly polymorphic reads ("all payments") favour single50 table; mostly per-subtype reads with heavy write integrity favour joined.514. **Establish whether the database must enforce the subtype's required fields.** If yes,52 single table is out unless you are prepared to write check constraints.535. **Establish the evolution rate.** A hierarchy that gains a subtype every quarter pays a54 migration per subtype under joined and concrete table, and may require columns, discriminator checks and indexes under single table.556. **Decide, and pin the discriminator values** explicitly, so a class rename is not a data56 migration.5758## Decision rules5960```text61Subtypes only share fields and have no distinct substitution/invariant contract62 → compare composition or mapped-superclass reuse. An enum with embedded63 groups may fit; JSON requires explicit validation/query/index trade-offs.6465Few subtypes, few subtype-specific columns, polymorphic queries common,66performance matters67 → SINGLE_TABLE often minimizes joins for polymorphic reads. Confirm68 row width, indexes, predicates and workload before claiming speed.69 Pay with nullable columns and weak database-level constraints.7071Many subtype-specific columns, or the database must enforce them, or72subtypes are large and distinct73 → JOINED. Normalised and constrained; measure joins for entity loads and inserts across mapped tables;74 base projections need not load every subtype.7576Subtypes are genuinely unrelated in storage terms, never queried77polymorphically, and no other table needs a foreign key to the base78 → TABLE_PER_CLASS. Rarely the right answer; an ordinary FK needs a single referenced table;79 hierarchy-wide queries usually require UNIONs or separate queries.8081The hierarchy is deep (3+ levels)82 → inspect mapped levels and actual fields/SQL before flattening.83 Depth can add joins; it does not itself add SINGLE_TABLE columns.8485Variation is per-tenant or per-configuration, and new variants must ship86without a deploy87 → not inheritance at all: data-driven variation, with the88 variant part in a serialized LOB (orm-structural-mapping).89```9091## Rules9293- **Single table's real cost includes constraint complexity, not just disk.** Columns may remain94 nullable at the column level while discriminator-aware `CHECK` constraints enforce “a95 `CardPayment` must have `card_last4`.” Verify ORM-generated DDL and bulk/import paths, or96 accept that only application code enforces it — and that bulk imports bypass it.97- **Joined entity materialization often joins mapped levels/subtypes**, while base-only98 projections can avoid them; writes span the tables holding the entity state. It is usually acceptable; it becomes painful on a list99 screen that reads the whole hierarchy at volume, and that is a measurable question, not a100 matter of taste (`architecture-and-performance`).101- Concrete table per class has no single base table for an ordinary polymorphic foreign key.102 Alternatives such as separate subtype FKs, a registry/base identity table or application-level103 integrity add complexity; price them before excluding the strategy.104- **Pin stored discriminator values** where used (`@DiscriminatorValue("CARD")`). For a105 STRING discriminator the default is the entity name; an unpinned entity name follows the106 class name. Preserve existing values during renames, including explicit entity/table names.107- Query costs depend on selected attributes, predicates, fetches and provider SQL. A root108 SINGLE_TABLE query need not filter the discriminator at all; fewer joins do not guarantee109 a cheap plan. Inspect plans and representative workload before comparing strategies.110- A hierarchy that only shares fields wants `@MappedSuperclass` (shared mapping, no111 polymorphism, no base table) or composition. `@MappedSuperclass` is under-used and is112 exactly right for audit columns and shared identifiers.113- Adding a subtype may need new columns/checks/indexes (single table), a new mapped table114 and FK (joined), or a concrete table. Locking and scan/rewrite costs depend on the exact115 database/version and DDL; additive does not mean online. Changing116 strategy later usually requires a substantial data migration. It is expensive, not literally117 one-way; use expand/contract, reconciliation and rollback/forward-fix analysis118 (`architecture-decision-making`).119- Choose indexes from predicates and selectivity. Partial/filtered indexes can help sparse120 subtype data where supported and where the query implies their predicate; inspect the121 generated, possibly parameterized SQL and plan.122- The domain question comes first. If `PremiumCustomer` and `StandardCustomer` differ only123 in a discount rate, they are one type with a policy, and no mapping strategy will make the124 subtype earn its keep (`domain-logic-organization`).125126For a recommendation, report query/DDL evidence, the integrity and migration trade-offs,127and the targeted validation that would confirm it. Without plans or workload data, keep128performance conclusions conditional.129130## References131132- [Strategy comparison](references/strategy-comparison.md) — the three strategies with133 their DDL, their generated SQL for the queries that matter, query-cost hypotheses,134 constraint enforcement including conditional check constraints, indexing under each, and135 the alternatives to inheritance with the shape that replaces it. Read when choosing a136 strategy or justifying an existing one.137- [Schema evolution](references/schema-evolution.md) — what adding, removing, splitting or138 moving a subtype costs under each strategy; discriminator value management and renames;139 migrating between strategies with an expand/contract sequence; and the read-model options140 when the write-side mapping is right but reads are expensive. Read before changing a141 hierarchy that has data in it.