ArchitectMind
  1. Home
  2. Tutorials
  3. Book 3 · Jakarta EE & Enterprise Java
  4. Volume IV
  5. ORM
Book 3 · Volume IV — Persistence & Transaction Management · Topic 1.1

ORM

Impedance mismatch, ORM concepts, Mapping

8 min readStage 1 · Foundations8 interview questions
On this page
  1. 1. · Concept & Definition
  2. 2. · Why It Matters
  3. 3. · How It Works
  4. 4. · Java / Jakarta EE Code Examples
  5. 5. · Configuration, Defaults & Diagnostics
  6. Deep Dive — Inheritance Mapping Strategies
  7. Deep Dive — Providers and Portability
  8. 6. · Production Usage Patterns
  9. 7. · Common Pitfalls & Misconceptions
  10. 8. · Best Practices
  11. 9. · Architect's Perspective & Trade-offs
  12. 10. · Interview Q&A

1· Concept & Definition

Object/relational mapping (ORM) is the technique of representing rows of relational tables as objects of a domain model and keeping the two synchronised automatically. Jakarta Persistence is the Jakarta EE standard API for ORM: applications annotate classes as entities, and a persistence provider generates the SQL needed to load, insert, update and delete them, tracks changes to loaded objects and translates object-oriented queries into SQL.

TermMeaning
EntityA class whose instances correspond to rows of a table, with a persistent identity
Persistence providerThe implementation (Hibernate, EclipseLink) that generates and executes SQL
Persistence unitA named set of entity classes plus a data source and provider configuration
EntityManagerThe API through which the application manipulates entities
Persistence contextThe set of entity instances managed by an EntityManager at a given time
DialectProvider configuration describing the SQL flavour of the target database

2· Why It Matters

  • ORM removes most hand-written CRUD SQL and mapping code, which is where a large share of defects used to live.
  • It provides a single place for transactional write behaviour: change tracking, ordering of statements, optimistic locking.
  • Its abstractions hide the database just enough to be dangerous: the SQL still exists, and architects must be able to predict it.
  • The mapping choices made early (identity, associations, fetch types, inheritance) are expensive to change later.

3· How It Works

Impedance mismatch

The object/relational impedance mismatch is the set of structural differences between an object model and a relational schema. ORM does not remove these differences; it provides a systematic translation, and every translation has a cost that shows up somewhere — usually as extra queries, surprising updates or constraints on how the domain model may be written.

Five dimensions of the impedance mismatch
Figure 1.1.1 — Five dimensions of the impedance mismatch
DimensionObject modelRelational modelORM answer
GranularityMany small classes, value objectsFewer, wider tables@Embeddable value types, @Convert
InheritanceClass hierarchies, polymorphismNo native inheritanceSINGLE_TABLE, JOINED, TABLE_PER_CLASS
Identity== and equals()Primary keyIdentity map per context, @Id
AssociationsDirected references, collectionsForeign keys (bidirectional by nature)Owning/inverse sides, join tables
Navigationorder.getCustomer().getAddress()Joins and set operationsLazy loading, fetch joins, entity graphs
Data retrievalWalk the graphSelect the columns you needProjections, DTOs, entity graphs

ORM concepts

ConceptWhat it gives youWhat to watch
Transparent persistenceChanges to loaded objects are written automaticallyUnintended updates from accidental setters
Unit of workStatements are collected and executed at flushTiming of SQL differs from the code order
Identity mapOne instance per PK per contextInstances differ across contexts
Lazy loadingLoad only what is traversedLazyInitializationException, N+1 queries
Object query languageQuery the model, not tablesGenerated SQL may not be the one you'd write
CachingFirst-level (context) and optional second-levelStaleness, invalidation in clusters

Mapping

Mapping declares how classes, fields and associations correspond to tables, columns and foreign keys. In Jakarta Persistence it is expressed with annotations on entity classes (or in an XML mapping file, which overrides annotations — useful when the same classes must map differently per deployment).

The four levels at which mapping is declared
Figure 1.1.2 — The four levels at which mapping is declared
Mapping areaMain annotations
Entity and table@Entity, @Table, @SecondaryTable, @Index, @UniqueConstraint
Identity@Id, @GeneratedValue, @EmbeddedId, @IdClass
Basic fields@Basic, @Column, @Enumerated, @Lob, @Convert, @Transient
Value objects@Embeddable, @Embedded, @AttributeOverride
Associations@ManyToOne, @OneToMany, @OneToOne, @ManyToMany, @JoinColumn, @JoinTable
Inheritance@Inheritance, @DiscriminatorColumn, @MappedSuperclass
Concurrency@Version

4· Java / Jakarta EE Code Examples

An entity with identity, columns, a value object and a version
@Entity
@Table(name = "orders",
       uniqueConstraints = @UniqueConstraint(name = "uk_order_number",
               columnNames = "order_number"))
public class Order {
 
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
    @SequenceGenerator(name = "order_seq", sequenceName = "order_seq",
            allocationSize = 50)
    private Long id;
 
    @Column(name = "order_number", nullable = false, updatable = false, length = 20)
    private String orderNumber;
 
    @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20)
    private OrderStatus status = OrderStatus.NEW;
 
    @Embedded
    // value object → columns in orders
    private Money total;
 
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt = Instant.now();
 
    @Version
    private long version;                                  // optimistic locking
    // getters/setters …
}
 
@Embeddable
public record Money(@Column(name = "total_amount") BigDecimal amount,
                    @Column(name = "total_currency", length = 3) String currency) { }
Persistence unit configuration
<!-- META-INF/persistence.xml -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.2">
  <persistence-unit name="shopPU" transaction-type="JTA">
    <jta-data-source>java:app/jdbc/shop</jta-data-source>
    <properties>
      <property name="jakarta.persistence.schema-generation.database.action"
              value="none"/>
      <property name="hibernate.jdbc.batch_size" value="50"/>
      <property name="hibernate.order_inserts" value="true"/>
      <property name="hibernate.order_updates" value="true"/>
    </properties>
  </persistence-unit>
</persistence>
Programmatic configuration (3.2)
// Jakarta Persistence 3.2: bootstrapping programmatically (useful in tests and Java SE)
EntityManagerFactory emf = new PersistenceConfiguration("shopPU")
        .provider("org.hibernate.jpa.HibernatePersistenceProvider")
        .managedClass(Order.class)
        .property("jakarta.persistence.jdbc.url", "jdbc:h2:mem:test")
        .createEntityManagerFactory();
SQLgenerated for the entity above
Hibernate: insert into orders
        (created_at,order_number,status,total_amount,total_currency,version,id)
           values (?,?,?,?,?,?,?)
Hibernate: update orders set status=?, version=? where id=? and version=?

5· Configuration, Defaults & Diagnostics

PropertyTypical valuePurpose
jakarta.persistence.schema-generation.database.actionnone in productionSchema creation is a migration-tool job (Flyway/Liquibase)
jakarta.persistence.sql-load-script-sourcetest data scriptSeeding in tests
hibernate.jdbc.batch_size20–100JDBC batching of inserts/updates
hibernate.order_inserts / order_updatestrueGroups statements so batching is effective
hibernate.generate_statisticstrue in test/perfQuery and entity counters
SQL loggingorg.hibernate.SQL at DEBUGSee the statements actually issued

Deep Dive — Inheritance Mapping Strategies

Class hierarchies are the sharpest part of the impedance mismatch, and the chosen strategy shows up directly in query plans.

StrategySchemaStrengthsWeaknesses
SINGLE_TABLE (default)One table, discriminator columnFastest (no joins), simple polymorphic queriesSubclass columns must be nullable; wide table
JOINEDTable per class, joined by PKNormalised, NOT NULL constraints possibleJoins on every read; slower polymorphic queries
TABLE_PER_CLASSOne table per concrete classNo joins for concrete readsPolymorphic queries become UNIONs; identity must be shared
@MappedSuperclassNo table for the base classShares mapped fields onlyNot polymorphic; cannot be queried as a type
Single-table inheritance
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type", length = 20)
public abstract class Payment {
    @Id @GeneratedValue private Long id;
    private BigDecimal amount;
}
 
@Entity @DiscriminatorValue("CARD")
public class CardPayment extends Payment { private String maskedPan; }
 
@Entity @DiscriminatorValue("UPI")
public class UpiPayment extends Payment { private String vpa; }
 
// SELECT p FROM Payment p  →  one SELECT with "where payment_type in ('CARD','UPI')"

Deep Dive — Providers and Portability

AspectStandard (Jakarta Persistence)Provider extension
Batchingnot specifiedhibernate.jdbc.batch_size, EclipseLink batch writing
Second-level cache@Cacheable, shared-cache-modeRegion configuration, providers (Infinispan, EHCache)
Statisticsnot specifiedHibernate Statistics, EclipseLink profiler
Multi-tenancynot specifiedHibernate tenant resolver strategies
Soft deletes, filtersnot specifiedHibernate @SoftDelete, @Filter

Portable code should keep provider-specific features behind configuration and a small number of clearly marked classes, so that an upgrade or provider change does not ripple through the domain model.

6· Production Usage Patterns

  • Entities used as the write model, with DTO projections for read-heavy endpoints.
  • Schema managed by migrations; the mapping validated against it at build or startup (validate).
  • A thin repository layer over EntityManager, or Jakarta Data repositories (Chapter 7).
  • Integration tests running against the real database engine (Testcontainers) rather than an in-memory substitute.

7· Common Pitfalls & Misconceptions

  • Treating entities as DTOs and exposing them directly in APIs — lazy loading and serialisation problems.
  • Letting the provider generate the schema in production instead of versioned migrations.
  • Assuming ORM makes SQL irrelevant — the generated statements decide performance.
  • Mapping every table even when a read-only query or a simple JDBC call would do.
  • @Enumerated(ORDINAL) (the default!) — adding an enum constant silently changes stored meanings.

8· Best Practices

  • Keep the domain model and schema evolving together through migrations, with mapping validation in CI.
  • Always specify @Enumerated(EnumType.STRING) and explicit column definitions for important fields.
  • Log and review generated SQL during development; count statements in tests.
  • Use value objects (@Embeddable) to keep the model expressive without extra tables.

9· Architect's Perspective & Trade-offs

ApproachStrengthsWeaknessesFits
Jakarta Persistence (ORM)Transparent writes, portability, rich modelHidden SQL, learning curveTransactional domain logic
SQL mapper (MyBatis, JDBI)Full SQL control, predictableManual mapping, no change trackingReport-heavy, legacy schemas
Plain JDBC / jOOQMaximum control, type-safe SQL (jOOQ)More code, no unit of workBulk operations, analytics
Jakarta Data repositoriesLess boilerplate over JPAStill JPA underneathCRUD and simple queries

10· Interview Q&A

Q1Basic

What is object/relational mapping?

Object-relational mapping is a technique for storing and loading objects in a relational database without writing SQL by hand for every operation. Classes are mapped to tables, fields to columns and associations to foreign keys, usually through annotations such as @Entity, @Id and @ManyToOne. The ORM provider generates SQL, tracks changes to loaded objects and writes them back at commit. In Jakarta EE the standard API is Jakarta Persistence, implemented by providers such as Hibernate and EclipseLink. ORM removes a lot of repetitive data-access code, but developers still need to understand the SQL it produces to avoid performance problems.

Q2Basic

What is a persistence unit?

A persistence unit is a named configuration that tells Jakarta Persistence which entity classes to manage, which data source to use and which provider and properties apply. Traditionally it is declared in META-INF/persistence.xml; Jakarta Persistence 3.2 also allows it to be defined programmatically. The application server or framework uses it to create an EntityManagerFactory, from which EntityManager instances are obtained. An application can have several persistence units, for example one per database. In Spring Boot the persistence unit is usually configured automatically from properties, but the concept is the same.

Q3Intermediate

What is the impedance mismatch?

The impedance mismatch is the set of structural differences between object models and relational schemas. Objects can be fine-grained, while tables group many values into rows. Objects use inheritance; tables have none. Objects have identity, equality and references, while rows have primary keys and foreign keys. Objects navigate associations by following references; databases use joins. An ORM translates between the two worlds, but it cannot remove the differences. They still influence design decisions, such as choosing an inheritance mapping strategy, and performance, such as lazy loading causing N+1 queries when an object graph is traversed carelessly.

Q4Intermediate

Why is @Enumerated(EnumType.STRING) recommended?

By default, an enum field is stored as its ordinal, its position in the enum declaration. If someone later inserts a new constant in the middle or reorders constants, existing rows silently take on a different meaning: an order stored as 2, meaning SHIPPED, might suddenly read as CANCELLED. @Enumerated(EnumType.STRING) stores the constant's name instead, which is stable when constants are added or reordered, and readable when querying the database directly. Renaming a constant still requires a data migration. For more control, a JPA AttributeConverter can map enums to explicit codes, which also keeps database values independent of Java names.

Q5Senior Engineer

How do you keep control over the SQL an ORM generates?

Make the SQL visible and testable. During development, enable SQL logging with bind parameters and the provider's statistics, so you can see how many statements each use case runs. In integration tests, assert statement counts for important use cases, so an accidental N+1 query fails the build. Compare generated DDL against the migration scripts. In code, choose fetch strategies deliberately: use fetch joins or entity graphs where related data is needed, projections or DTO queries for read-only views, and JDBC batching for bulk inserts and updates. In production, monitor slow queries at the database level.

Q6Senior Engineer

When would you not use JPA for a piece of functionality?

JPA is designed for loading, changing and saving entities in transactional use cases. It is a poor fit for bulk, set-based operations such as updating a million rows, where loading each entity is wasteful; for reporting queries with heavy aggregation and many joins; and for database-specific features such as window functions, recursive queries or stored procedures. For these, use native SQL, a query library such as jOOQ, or database-side code, often alongside JPA in the same application. The key is to choose per use case and keep the boundary clear, so each part uses the tool that fits.

Q7Technical Architect

How should schema evolution be managed in a JPA application?

Treat the database schema as code with its own history. Write versioned migration scripts with Flyway or Liquibase and make them the single source of truth; they run automatically at deployment, in the same order in every environment. Disable the provider's automatic schema generation in all shared environments, because it cannot handle data migrations or renames safely. Use the provider's validation mode or integration tests to check that entity mappings match the migrated schema, so mismatches fail early. For zero-downtime deployments, design migrations in expand-and-contract steps so old and new application versions can run against the same schema.

Q8Solution Architect

What architectural risks does ORM introduce in large systems?

The first risk is hidden cost: an innocent-looking navigation through an object graph can trigger hundreds of queries, so latency and database load grow unseen until production traffic arrives. The second is coupling: a large entity model shared across many teams or bounded contexts becomes a place where everyone's changes collide, much like a shared database. The third is scalability: a model designed for convenience can force chatty access patterns that are hard to optimise later. Mitigate them with per-use-case queries and DTOs, separate models per bounded context, measured SQL in tests, and clear guidance on when to bypass the ORM.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.