Using @ConcreteProxy in Hibernate
Hibernate is a powerful ORM framework, but its use of proxies for lazy loading can sometimes lead to subtle and confusing issues, especially when inheritance is involved. The @ConcreteProxy annotation was introduced to address one such issue: incorrect proxy type resolution. Let us delve into understanding Hibernate ConcreteProxy and its role in object persistence and lazy loading.
1. Introduction to Hibernate
Hibernate is a Java-based Object Relational Mapping (ORM) framework that simplifies database interactions by mapping Java classes to relational database tables. It allows developers to work with persistent data using object-oriented concepts instead of writing repetitive and error-prone SQL queries. Hibernate sits between the application and the database, handling tasks such as connection management, SQL generation, transaction handling, and result set mapping. This leads to cleaner code, improved productivity, and better maintainability.
Key features provided by Hibernate include:
- Automatic persistence: Hibernate automatically synchronizes Java object state with the database using CRUD operations without requiring explicit SQL.
- Lazy and eager loading: Associations between entities can be loaded on demand (lazy) or upfront (eager) to optimize performance.
- First-level and second-level caching: Hibernate caches entities to reduce database calls and improve application performance.
- Inheritance mapping: Supports multiple inheritance strategies such as single table, joined table, and table-per-class.
- Database independence: Hibernate generates database-specific SQL, allowing applications to switch databases with minimal changes.
1.1 Understanding the @ConcreteProxy Annotation
@ConcreteProxy is a Hibernate-specific annotation introduced in Hibernate 6. It controls how Hibernate creates proxy objects for entities when lazy loading is enabled. By default, Hibernate may generate proxies based on a mapped superclass or an interface. When @ConcreteProxy is applied, Hibernate is instructed to create proxies using the concrete entity class itself. This behavior is particularly useful in inheritance hierarchies where accurate runtime typing is important. It ensures that:
- The proxy class matches the actual entity subclass stored in the database
instanceofchecks behave as expected- Downcasting does not result in
ClassCastException
In summary, @ConcreteProxy improves type safety and predictability when working with lazy-loaded entities, especially in polymorphic and inheritance-based domain models.
2. Issues with Proxy Type Resolution in Hibernate
Hibernate relies heavily on proxy objects to implement lazy loading. A proxy acts as a lightweight placeholder that defers database access until the entity is actually used. While this improves performance, it can introduce challenges when inheritance is involved. By default, Hibernate generates proxies based on the declared association type, not the actual concrete entity stored in the database. Consider the following inheritance hierarchy:
Payment (abstract) ├── CreditCardPayment └── UpiPayment
If an association is declared as type Payment, Hibernate may create a proxy of type Payment even when the underlying database row represents a CreditCardPayment or UpiPayment.
This mismatch between the proxy type and the actual entity type can cause several subtle and hard-to-debug issues:
instanceofchecks failing: Runtime type checks may return false because the proxy does not reflect the concrete subclass.ClassCastException: Casting a proxy to a specific subclass can fail, even though the data represents that subclass.- Incorrect JSON serialization: Serialization frameworks (such as Jackson) may serialize the proxy as the base type, losing subclass-specific fields.
- Broken business logic: Domain logic that depends on concrete behavior or overridden methods may not execute as expected.
These issues are especially common in polymorphic domain models where behavior varies by subclass and runtime type accuracy is critical.
The @ConcreteProxy annotation addresses this problem by instructing Hibernate to always generate proxies using the actual concrete subclass. As a result:
- The proxy type matches the real entity type
- Type checks and casts behave correctly
- Serialization frameworks receive the correct class information
- Polymorphic business logic remains reliable
In short, @ConcreteProxy ensures consistent and predictable behavior when working with lazy-loaded entities in inheritance hierarchies.
3. Practical Code Demonstration
3.1 Setting Up the Project
The following Maven dependencies set up Hibernate with JPA support and an in-memory database for local development and testing.
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>stable__jar__version</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>stable__jar__version</version>
</dependency>
</dependencies>
Here, hibernate-core provides the ORM implementation, jakarta.persistence-api supplies the standard JPA annotations and interfaces, and the H2 database is included as a lightweight, in-memory datastore suitable for development, demos, and integration testing.
3.2 Defining the Entity Models
3.2.1 Base Entity: Payment.java
This abstract base entity defines the common structure for all payment types and configures Hibernate inheritance and proxy behavior.
package com.example.hibernate;
import jakarta.persistence.*;
import org.hibernate.annotations.ConcreteProxy;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type")
@ConcreteProxy
public abstract class Payment {
@Id
@GeneratedValue
private Long id;
private double amount;
public Long getId() {
return id;
}
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
}
Here, @Entity marks the class as a persistent JPA entity, @Inheritance(SINGLE_TABLE) stores all subclasses in a single database table distinguished by the payment_type discriminator column, and @ConcreteProxy ensures Hibernate creates lazy-loading proxies using the actual concrete subclass (such as CreditCardPayment or UpiPayment) instead of the abstract Payment type.
3.2.2 Concrete Entity: CreditCardPayment.java
This concrete entity represents a credit card–based payment and extends the shared payment model.
package com.example.hibernate;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("CC")
public class CreditCardPayment extends Payment {
private String cardNumber;
public String getCardNumber() {
return cardNumber;
}
public void setCardNumber(String cardNumber) {
this.cardNumber = cardNumber;
}
}
In this class, @Entity marks it as a persistent subclass of Payment, @DiscriminatorValue("CC") tells Hibernate to store and identify this type using the value CC in the discriminator column, and the cardNumber field captures credit card–specific data that is only applicable to this concrete payment type.
3.2.3 Concrete Entity: UpiPayment.java
This concrete entity models a UPI-based payment and extends the common payment abstraction.
package com.example.hibernate;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("UPI")
public class UpiPayment extends Payment {
private String upiId;
public String getUpiId() {
return upiId;
}
public void setUpiId(String upiId) {
this.upiId = upiId;
}
}
Here, @Entity registers the class as a persistent subclass of Payment, @DiscriminatorValue("UPI") maps this implementation to the UPI value in the discriminator column, and the upiId field stores UPI-specific information that is unique to this payment type and not shared by other subclasses.
3.2.4 Order Entity: OrderEntity.java
This entity represents an order that references a payment using a polymorphic association.
package com.example.hibernate;
import jakarta.persistence.*;
@Entity
@Table(name = "orders")
public class OrderEntity {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Payment payment;
public Long getId() {
return id;
}
public Payment getPayment() {
return payment;
}
public void setPayment(Payment payment) {
this.payment = payment;
}
}
In this example, @ManyToOne(fetch = FetchType.LAZY) defines a lazy-loaded association to the abstract Payment type, which is exactly where proxy type resolution becomes important, and when combined with @ConcreteProxy on Payment, Hibernate ensures that the lazily loaded proxy resolves to the actual concrete subclass such as CreditCardPayment or UpiPayment at runtime.
3.3 Hibernate Utility Configuration
This utility class bootstraps Hibernate programmatically and provides a reusable SessionFactory for the application.
package com.example.hibernate;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
public class HibernateUtil {
private static final SessionFactory SESSION_FACTORY;
static {
StandardServiceRegistry registry =
new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.driver_class", "org.h2.Driver")
.applySetting("hibernate.connection.url", "jdbc:h2:mem:testdb")
.applySetting("hibernate.hbm2ddl.auto", "create-drop")
.applySetting("hibernate.show_sql", "true")
.applySetting("hibernate.format_sql", "true")
.build();
SESSION_FACTORY = new MetadataSources(registry)
.addAnnotatedClass(Payment.class)
.addAnnotatedClass(CreditCardPayment.class)
.addAnnotatedClass(UpiPayment.class)
.addAnnotatedClass(OrderEntity.class)
.buildMetadata()
.buildSessionFactory();
}
public static SessionFactory getSessionFactory() {
return SESSION_FACTORY;
}
}
Here, the StandardServiceRegistryBuilder configures the H2 in-memory database, schema generation strategy, and SQL logging, while MetadataSources registers all annotated entities so Hibernate can build the metadata model and create a single SessionFactory that is reused across the application lifecycle.
3.4 Main Application Class
This class demonstrates persisting entities and verifying how Hibernate resolves proxy types at runtime when lazy loading is involved.
package com.example.hibernate;
import org.hibernate.Session;
public class Main {
public static void main(String[] args) {
Session session = HibernateUtil.getSessionFactory().openSession();
session.beginTransaction();
// Create payment
CreditCardPayment payment = new CreditCardPayment();
payment.setAmount(1500);
payment.setCardNumber("4111-1111-1111-1111");
// Create order
OrderEntity order = new OrderEntity();
order.setPayment(payment);
session.persist(payment);
session.persist(order);
session.getTransaction().commit();
session.clear();
// Fetch order (payment is LAZY)
session.beginTransaction();
OrderEntity fetchedOrder =
session.find(OrderEntity.class, order.getId());
Payment fetchedPayment = fetchedOrder.getPayment();
System.out.println("Payment class: " + fetchedPayment.getClass().getName());
if (fetchedPayment instanceof CreditCardPayment) {
System.out.println("Concrete proxy resolved correctly");
}
session.getTransaction().commit();
session.close();
}
}
In this flow, the payment and order are first persisted, the session is cleared to force lazy loading on retrieval, and when the Payment association is accessed, Hibernate creates a proxy that resolves to the concrete CreditCardPayment class, allowing the instanceof check to succeed and clearly demonstrating the effect of @ConcreteProxy.
3.5 Code Run and Output
When the application is executed, Hibernate first creates the database schema, persists the entities, and then reloads the order to trigger lazy loading of the associated payment.
During the second transaction, accessing fetchedOrder.getPayment() causes Hibernate to initialize a proxy for the payment association, and because @ConcreteProxy is enabled, the proxy is created using the actual concrete subclass.
Payment class: com.example.hibernate.CreditCardPayment Concrete proxy resolved correctly
This output confirms that Hibernate correctly resolves the proxy to CreditCardPayment rather than the abstract Payment type, validating that @ConcreteProxy preserves accurate runtime typing and ensures polymorphic behavior works as expected with lazy-loaded associations.
4. Conclusion
@ConcreteProxy is a small but powerful annotation that solves a long-standing issue with Hibernate’s lazy-loading proxies in inheritance hierarchies. By ensuring that proxies reflect the actual concrete entity type, it improves type safety, prevents runtime casting issues, simplifies polymorphic business logic, and makes debugging and serialization more predictable. If you are using Hibernate 6+ and working with inheritance and lazy associations, @ConcreteProxy is strongly recommended.

