Most Spring Boot developers would struggle to answer the question: which Hibernate bootstrap path are you actually using? The answer is “the one Spring Boot chose for you”, which is the EntityManagerFactory path, built by HibernateJpaAutoConfiguration, wired to a HikariDataSource, reading from application.properties. You have never typed a single line of Hibernate bootstrap code.
That is fine for the common case. But when something breaks at startup — the wrong dialect is selected, HikariCP sizing is wrong, the schema validation fails — you need to know what is actually being configured and where. And for projects outside Spring Boot (command-line tools, framework libraries, Jakarta EE deployments), you need to choose a bootstrap path and implement it deliberately.
This post covers all three paths, when each makes sense, and the production failure that catches developers who autoconfig their way to production without understanding what they built.
1. The Three Bootstrap Paths
| Path | API | Standard | Jakarta EE portable | Spring Boot compatible |
|---|---|---|---|---|
| Hibernate-native SessionFactory | StandardServiceRegistryBuilder + MetadataSources | Hibernate only | No | Requires manual configuration |
| JPA EntityManagerFactory (programmatic) | Persistence.createEntityManagerFactory() or PersistenceProvider.createContainerEntityManagerFactory() | Jakarta Persistence | Yes | Works with Spring’s LocalContainerEntityManagerFactoryBean |
| Spring Boot auto-config | HibernateJpaAutoConfiguration | Spring | No (Spring-specific) | Default |
2. When to Choose Each
Profile A: CLI tool or library that uses Hibernate directly
A data migration tool, a code generator, a schema inspection utility. No web container, no Spring context. The Hibernate-native SessionFactory path is the right choice: full control, no framework overhead, minimal dependencies. You configure what you need and nothing more.
Profile B: Jakarta EE application or multi-provider JPA requirement
A service running on Payara, WildFly, or OpenLiberty, or a codebase that must be JPA-provider-agnostic. Use the programmatic EntityManagerFactory path with persistence.xml. The container manages the lifecycle; your code only depends on the jakarta.persistence API. This is the most portable option.
Profile C: Spring Boot service
Use Spring Boot auto-config. Configure via application.properties. If you need to override specific behaviour (custom dialect, additional properties, multi-datasource), extend with a @Bean for LocalContainerEntityManagerFactoryBean and let Spring merge your overrides with the defaults. Writing a manual SessionFactory setup in a Spring Boot app is fighting the framework.
3. Hibernate-Native SessionFactory Bootstrap
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.AvailableSettings;
public final class HibernateUtil {
private static final SessionFactory SESSION_FACTORY = buildSessionFactory();
private static SessionFactory buildSessionFactory() {
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting(AvailableSettings.JAKARTA_JDBC_URL,
System.getenv("DB_URL"))
.applySetting(AvailableSettings.JAKARTA_JDBC_USER,
System.getenv("DB_USER"))
.applySetting(AvailableSettings.JAKARTA_JDBC_PASSWORD,
System.getenv("DB_PASSWORD"))
// Dialect auto-detection: omit DIALECT for Hibernate 7
// Setting it manually for a slow-to-connect DB can cause startup failure
.applySetting(AvailableSettings.HBM2DDL_AUTO, "validate")
.applySetting(AvailableSettings.SHOW_SQL, false)
.applySetting(AvailableSettings.FORMAT_SQL, true)
.applySetting(AvailableSettings.STATEMENT_BATCH_SIZE, 50)
.applySetting(AvailableSettings.ORDER_INSERTS, true)
.applySetting(AvailableSettings.ORDER_UPDATES, true)
.build();
try {
Metadata metadata = new MetadataSources(registry)
.addAnnotatedClass(Order.class)
.addAnnotatedClass(OrderItem.class)
.addAnnotatedClass(Customer.class)
.getMetadataBuilder()
.build();
SessionFactory sf = metadata.getSessionFactoryBuilder().build();
Runtime.getRuntime().addShutdownHook(
new Thread(() -> {
if (!sf.isClosed()) sf.close();
})
);
return sf;
} catch (Exception ex) {
StandardServiceRegistryBuilder.destroy(registry);
throw new ExceptionInInitializerError("Hibernate bootstrap failed: " + ex.getMessage());
}
}
public static SessionFactory get() { return SESSION_FACTORY; }
}
Key decisions in this setup: credentials from environment variables (never hardcoded); DIALECT deliberately omitted (Hibernate 7 detects it from JDBC metadata — why this matters is covered in section 6); hbm2ddl.auto=validate for production (never update); the shutdown hook closes the factory cleanly on SIGTERM. The registry is destroyed if factory creation fails to prevent resource leaks.
4. EntityManagerFactory Programmatic Bootstrap
The JPA-standard equivalent, using a persistence.xml file for entity registration but programmatic property overrides:
<!-- src/main/resources/META-INF/persistence.xml -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
version="3.2">
<persistence-unit name="orders-pu" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>com.example.Order</class>
<class>com.example.OrderItem</class>
<class>com.example.Customer</class>
<properties>
<property name="jakarta.persistence.schema-generation.database.action" value="validate"/>
</properties>
</persistence-unit>
</persistence>
Map<String, String> overrides = Map.of(
"jakarta.persistence.jdbc.url", System.getenv("DB_URL"),
"jakarta.persistence.jdbc.user", System.getenv("DB_USER"),
"jakarta.persistence.jdbc.password", System.getenv("DB_PASSWORD"),
"hibernate.jdbc.batch_size", "50",
"hibernate.order_inserts", "true"
);
EntityManagerFactory emf = Persistence.createEntityManagerFactory("orders-pu", overrides);
// Use inside a try-with-resources or manage lifecycle manually:
try (EntityManager em = emf.createEntityManager()) {
em.getTransaction().begin();
// ... operations ...
em.getTransaction().commit();
}
This path is fully portable: the code above works against EclipseLink, OpenJPA, or any other JPA provider just by swapping the provider class in persistence.xml. The programmatic overrides allow environment-specific credentials without modifying the XML file at deploy time, which matters for container image immutability.
5. What Spring Boot Auto-Config Actually Does
When Spring Boot detects Hibernate on the classpath, HibernateJpaAutoConfiguration backs JpaBaseConfiguration, which creates a LocalContainerEntityManagerFactoryBean. That bean wraps a HibernateJpaPersistenceProvider and produces an EntityManagerFactory that Spring manages.
The key properties that flow from application.properties into the Hibernate configuration:
Spring Boot’s HikariCP auto-configuration sets up the connection pool from spring.datasource.hikari.* properties. HikariCP defaults are aggressive: maximumPoolSize=10, connectionTimeout=30000. For a high-concurrency service these are often too small; the defaults are tuned for typical web apps, not for microservices handling burst traffic.
The entity scanning mechanism uses @SpringBootApplication‘s component scan base package by default. Entities that live outside the scan tree require explicit @EntityScan. Missing this produces a runtime error on first query, not at startup, which is one of the more frustrating Spring Boot Hibernate integration debugging scenarios.
6. The Dialect Detection Bug That Bites in Production
Hibernate 7 detects the database dialect automatically by opening a JDBC connection and querying DatabaseMetaData. This is an improvement over earlier versions where you had to specify the dialect class manually. But it introduces a timing dependency: the database must be reachable during application startup, before Hibernate finishes building the SessionFactory.
In containerised deployments, this is a real problem. The application container starts, Hibernate tries to detect the dialect, the database container is still initialising. The detection fails. Hibernate either falls back to a generic dialect (wrong SQL generated) or the build fails entirely with ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment].
The symptoms are subtle: the application starts, queries run, but some DB-specific syntax (like RETURNING clause for PostgreSQL, or ILIKE) does not appear in generated SQL because Hibernate picked a generic dialect. Or worse, sequences use the wrong prefix because Hibernate assumed MySQL IDENTITY strategy instead of PostgreSQL SEQUENCE.
The two fixes:
Option 1: Specify the dialect explicitly for production environments where you control the DB version:
Option 2: Use Docker Compose healthchecks or Kubernetes init-containers to ensure the database is ready before the application starts. This is the correct architectural fix; the dialect option is the tactical workaround.
Bad → Improved: Configuration.configure() Ordering
This is a legacy pattern still found in old Hibernate codebases that were recently “upgraded” to Hibernate 7 without changing the bootstrap approach.
Bad — legacy Configuration-based bootstrap with wrong ordering:
// Legacy approach — still works in Hibernate 7 but has ordering traps
Configuration configuration = new Configuration();
configuration.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
configuration.setProperty("hibernate.hbm2ddl.auto", "validate");
// configure() reads hibernate.cfg.xml and OVERWRITES any properties set above
// if hibernate.cfg.xml also sets hibernate.dialect, your setProperty call above is lost
configuration.configure(); // wrong position
SessionFactory sf = configuration.buildSessionFactory(
new StandardServiceRegistryBuilder()
.applySettings(configuration.getProperties())
.build()
);
Improved — configure() first, then apply overrides:
// configure() establishes the base from hibernate.cfg.xml
Configuration configuration = new Configuration();
configuration.configure(); // reads XML first
// Now apply overrides that take precedence over XML
configuration.setProperty("hibernate.hbm2ddl.auto", "validate"); // override any create/update in XML
configuration.setProperty("hibernate.show_sql", "false"); // ensure prod-safe setting
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySettings(configuration.getProperties())
.build();
SessionFactory sf = configuration.buildSessionFactory(registry);
The Configuration-based API is considered legacy in Hibernate 7. The StandardServiceRegistryBuilder + MetadataSources path shown in section 3 is the recommended replacement. Migrating to it also forces explicit entity registration, which surfaces missing entity errors at startup rather than at first query.
See Also
- 📘 Hibernate 7 Hello World — end-to-end working project with Maven dependencies and persistence.xml
- 📘 Spring Boot 4 + Hibernate 7 Configuration — what changed between Spring Boot 3/Hibernate 6 and Spring Boot 4/Hibernate 7
- 📘 HikariCP + Hibernate 7 — pool sizing math, production defaults, JMX monitoring
- 📘 Hibernate 7 Second-Level Cache — configuring Ehcache 3 as part of the SessionFactory bootstrap
Frequently Asked Questions
Should I use SessionFactory or EntityManagerFactory in a Spring Boot app?
Spring Boot auto-creates an EntityManagerFactory wrapping Hibernate’s SessionFactory. You can obtain the underlying SessionFactory from it: emf.unwrap(SessionFactory.class). For most Spring Boot code, work with EntityManager (JPA standard) rather than Session (Hibernate-specific). The exception is when you need Hibernate-specific features like StatelessSession for batch jobs.
Is hibernate.cfg.xml still supported in Hibernate 7?
Yes. The Configuration.configure() API and hibernate.cfg.xml still work in Hibernate 7. They are considered legacy: no new features are added to the XML-based path, and the documentation steers new projects toward the programmatic StandardServiceRegistryBuilder approach. Existing projects that work on the XML path do not need to migrate unless they are hitting specific limitations.
When does hbm2ddl.auto=update break in production?
update is unsafe in production for two reasons: it cannot remove columns (so removed entity fields leave orphan columns), and on a slow or busy database it attempts schema changes while the application is starting, which can take locks and block other queries. Use validate in production and manage schema migrations with Flyway or Liquibase instead.
Conclusion
The right bootstrap path depends on the deployment context. For Spring Boot applications the auto-config path is correct, and understanding what it does under the hood — the LocalContainerEntityManagerFactoryBean, the HikariCP defaults, the entity scanning scope — is what separates developers who can debug startup failures from developers who restart containers hoping the problem goes away. For Jakarta EE deployments, the persistence.xml path gives portability. For standalone tools, the native StandardServiceRegistryBuilder path gives control. Across all three, the dialect detection timing issue is the most common production surprise: specify the dialect explicitly if you cannot guarantee database availability before Hibernate starts its metadata query.