Best way of handling entities inheritance in Spring Data JPA
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Entity inheritance in JPA looks attractive because it maps an object-oriented model to an object-oriented language. The hard part is that relational databases do not have inheritance, so every strategy is a compromise between query simplicity, storage layout, and future schema changes.
Start With the Domain, Not the Annotation
The best strategy depends on what the parent and child entities represent in the database. If the subclasses are genuinely different records with a small shared core, normalized tables usually age better. If the hierarchy is shallow and reads dominate, a single table can be simpler.
Spring Data JPA supports three standard strategies:
- '
SINGLE_TABLE' - '
JOINED' - '
TABLE_PER_CLASS'
In practice, JOINED is often the safest default for business applications because it preserves a normalized schema while still letting you query the hierarchy through the base type.
When JOINED Is the Best Tradeoff
With JOINED, the base entity keeps shared fields and each subclass stores only its specific fields. Reads of a subclass require a join, but the schema remains clean and constraints are easier to reason about.
A repository for the base type works as expected:
Saving and loading remain straightforward:
This strategy is a good fit when you need polymorphic queries such as “find all content items,” but you still care about column constraints and a database design that does not turn into a large sparse table.
When SINGLE_TABLE Is Better
SINGLE_TABLE stores the whole hierarchy in one table and uses a discriminator column to identify the subtype. This reduces joins and can perform well for read-heavy workloads.
Choose it when these conditions are true:
- the hierarchy is small
- most fields are shared
- null-heavy child columns are acceptable
- reporting queries frequently scan the full hierarchy
The tradeoff is schema quality. As subclasses diverge, the table accumulates many nullable columns. Database-level validation also becomes weaker because columns required by one subtype must often stay nullable for others.
Why TABLE_PER_CLASS Is Usually a Last Resort
TABLE_PER_CLASS creates a full table for each concrete subclass. That sounds simple until you query the base type. Polymorphic queries become expensive because the provider must combine rows from several tables.
It can work for small systems that rarely query across the hierarchy, but it is usually not the first choice in Spring Data JPA applications. It also complicates identity generation and can surprise teams later when generic repository methods are used more heavily.
Repository Design Still Matters
Even with the right inheritance mapping, repository boundaries can make the design easier or harder to maintain. Two patterns usually work well:
- keep a base repository only for behavior that genuinely applies to every subtype
- add subtype repositories when queries are subtype-specific
This avoids forcing subtype logic into a generic repository that becomes hard to read.
Common Pitfalls
One common mistake is choosing SINGLE_TABLE too early because it looks simpler in code. If the subclasses evolve independently, the database becomes cluttered and migrations get noisy.
Another mistake is assuming inheritance is always better than composition. If the child entities do not need true polymorphic behavior, separate entities with shared embeddables may be cleaner.
A third issue is using eager relationships on top of JOINED. The extra joins from inheritance plus eager loading can create slow queries quickly. Prefer explicit fetch strategies and measure generated SQL.
Finally, do not let the object model hide the database cost. Turn on SQL logging in development and check what findAll() on the base repository actually does.
Summary
- '
JOINEDis often the best default because it balances schema quality and polymorphic access.' - '
SINGLE_TABLEis useful for shallow hierarchies with read-heavy workloads.' - '
TABLE_PER_CLASSis usually harder to scale and maintain.' - Use subtype repositories for subtype-specific queries instead of forcing everything through the base type.
- Validate the choice by looking at generated SQL and migration impact, not just annotation convenience.

