Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project

May 13, 2026 · View on GitHub

Purpose

This guide provides step-by-step instructions for configuring an Ebean Database bean using Avaje Inject (@Factory / @Bean), backed by a PostgreSQL datasource built with Ebean's DataSourceBuilder. Follow every step in order. This is Step 3 of 3.


Prerequisites

  • Step 1 complete: pom.xml already includes ebean-postgres, ebean-maven-plugin, and querybean-generator (see add-ebean-postgres-maven-pom.md)
  • Step 2 complete: Test container setup is working and mvn verify passes (see add-ebean-postgres-test-container.md)
  • Avaje Inject is on the classpath (e.g. io.avaje:avaje-inject)
  • A configuration source is available at runtime (e.g. avaje-config reading application.yml or environment variables)
  • The following configuration keys are resolvable at runtime (adapt names to your project):
    KeyDescription
    db_urlJDBC URL for the master/write connection
    db_userDatabase username
    db_passDatabase password
    db_master_min_connectionsMinimum pool size (default: 1)
    db_master_initial_connectionsInitial pool size at startup — set high to pre-warm on pod start (see K8s note below)
    db_master_max_connectionsMaximum pool size (default: 200)

Step 1 — Locate or create the @Factory class

Look for an existing Avaje Inject @Factory-annotated class in the project (often named AppConfig, DatabaseConfig, or similar). If one exists, add the new @Bean method to it. If none exists, create one:

package com.example.configuration;

import io.avaje.inject.Bean;
import io.avaje.inject.Factory;

@Factory
class DatabaseConfig {
    // beans will be added in the steps below
}

Step 2 — Add the Database bean method (minimal — master datasource only)

Add the following @Bean method to the @Factory class. This creates an Ebean Database backed by a single master (read-write) PostgreSQL datasource.

import io.ebean.Database;
import io.ebean.datasource.DataSourceBuilder;

@Bean
Database database() {
    var dataSource = DataSourceBuilder.create()
        .url(/* resolve from config, e.g.: */ Config.get("db_url"))
        .username(Config.get("db_user"))
        .password(Config.get("db_pass"))
        .driver("org.postgresql.Driver")
        .schema("myschema")                    // set to your target schema
        .applicationName("my-app")             // visible in pg_stat_activity
        .minConnections(Config.getInt("db_master_min_connections", 1))
        .initialConnections(Config.getInt("db_master_initial_connections", 10))
        .maxConnections(Config.getInt("db_master_max_connections", 200));

    return Database.builder()
        .name("db")                            // logical name for this Database instance
        .dataSourceBuilder(dataSource)
        .build();
}

Field guidance

FieldNotes
urlFull JDBC URL, e.g. jdbc:postgresql://host:5432/dbname
schemaThe Postgres schema Ebean should use (omit if using public)
applicationNameShown in pg_stat_activity.application_name; helps with DB-side diagnostics
name("db")Logical Ebean database name; relevant if multiple Database instances exist
minConnectionsConnections kept open at all times; pool will not shrink below this
initialConnectionsConnections opened at startup; see K8s warm-up note below
maxConnectionsHard upper limit on concurrent connections

Connection pool sizing for Kubernetes (and similar orchestrated environments)

When a pod starts in Kubernetes it will receive live traffic as soon as it passes readiness checks — often before the connection pool has had a chance to grow to handle the load. This can cause latency spikes on the first wave of requests while the pool expands one connection at a time.

Use initialConnections to pre-warm the pool at startup so it is already sized for peak load when the pod goes live:

minConnections:     2    ← floor; pool will shrink back here when idle
initialConnections: 20   ← opened at pod start, before first request arrives
maxConnections:     50   ← hard ceiling

The lifecycle is:

  1. Pod starts — pool opens initialConnections connections immediately.
  2. Pod receives traffic — pool is already at capacity; no growth latency.
  3. Traffic drops — idle connections are closed; pool trims back toward minConnections.
  4. Next traffic spike — pool grows again up to maxConnections on demand.

Set initialConnections to a value high enough that the pool does not need to grow during the first minute of live traffic. A common starting point is 50–75% of maxConnections.


Rather than calling Config.get(...) inline, inject a typed config helper or the Avaje Configuration bean if one is available. This makes the factory testable and keeps the wiring explicit. For example:

@Bean
Database database(Configuration config) {
    String url  = config.get("db_url");
    String user = config.get("db_user");
    String pass = config.get("db_pass");
    int    min  = config.getInt("db_master_min_connections", 1);
    int    init = config.getInt("db_master_initial_connections", 10);
    int    max  = config.getInt("db_master_max_connections", 200);

    var dataSource = DataSourceBuilder.create()
        .url(url)
        .username(user)
        .password(pass)
        .driver("org.postgresql.Driver")
        .schema("myschema")
        .applicationName("my-app")
        .minConnections(min)
        .initialConnections(init)
        .maxConnections(max);

    return Database.builder()
        .name("db")
        .dataSourceBuilder(dataSource)
        .build();
}

If the project has a dedicated config-wrapper class (a @Component that reads config keys), accept it as a parameter instead of Configuration.

Note: Injecting Configuration requires that avaje-config is properly wired into the DI context. If you encounter "No dependency provided for io.avaje.config.Configuration" errors, use Config.get(...) static access instead (as shown in Step 2).


Step 4 (Optional) — Add a read-only datasource

For production services that have a separate read-replica, add a second DataSourceBuilder for read-only queries and wire it via readOnlyDataSourceBuilder(...). The read-only datasource:

  • Uses readOnly(true) and autoCommit(true) (Ebean routes read queries there automatically)
  • Typically has a higher max connection count than the master
  • Benefits from a prepared-statement cache (pstmtCacheSize)
@Bean
Database database(Configuration config) {
    String masterUrl   = config.get("db_url");
    String readOnlyUrl = config.get("db_url_readonly");
    String user        = config.get("db_user");
    String pass        = config.get("db_pass");

    var masterDataSource = buildDataSource(user, pass)
        .url(masterUrl)
        .minConnections(config.getInt("db_master_min_connections", 1))
        .initialConnections(config.getInt("db_master_initial_connections", 10))
        .maxConnections(config.getInt("db_master_max_connections", 50));

    var readOnlyDataSource = buildDataSource(user, pass)
        .url(readOnlyUrl)
        .readOnly(true)
        .autoCommit(true)
        .pstmtCacheSize(250)          // cache up to 250 prepared statements per connection
        .maxInactiveTimeSecs(600)     // close idle connections after 10 minutes
        .minConnections(config.getInt("db_readonly_min_connections", 2))
        .initialConnections(config.getInt("db_readonly_initial_connections", 10))
        .maxConnections(config.getInt("db_readonly_max_connections", 200));

    return Database.builder()
        .name("db")
        .dataSourceBuilder(masterDataSource)
        .readOnlyDataSourceBuilder(readOnlyDataSource)
        .build();
}

private static DataSourceBuilder buildDataSource(String user, String pass) {
    return DataSourceBuilder.create()
        .username(user)
        .password(pass)
        .driver("org.postgresql.Driver")
        .schema("myschema")
        .applicationName("my-app")
        .addProperty("prepareThreshold", "2");   // PostgreSQL: server-side prepared statements
}

Additional configuration keys for the read-only datasource

KeyDescriptionDefault
db_url_readonlyJDBC URL for the read replica
db_master_initial_connectionsInitial master pool size at startup10
db_readonly_min_connectionsMinimum pool size2
db_readonly_initial_connectionsInitial pool size at startupsame as min
db_readonly_max_connectionsMaximum pool size20

Step 5 (Optional) — Enable the migration runner

If the project uses Ebean's built-in DB migration runner to apply SQL migrations on startup, enable it on the DatabaseBuilder:

return Database.builder()
    .name("db")
    .dataSourceBuilder(dataSource)
    .runMigration(true)          // run pending migrations on startup
    .build();

This is equivalent to setting ebean.migration.run=true in application.properties but is preferred because it keeps all database configuration in one place. To make it conditional (e.g. only in non-production environments):

.runMigration(config.getBoolean("db.runMigrations", false))

See the DB migration generation guide (add-ebean-db-migration-generation.md) for full details on generating and managing migration files.


See Also

For advanced connection pool configuration, production deployment patterns, and connection validation best practices, see the ebean-datasource guides:

  • Creating DataSource Pools — Covers read-only pools (readOnly(true) + autoCommit(true)), Kubernetes deployment strategies using initialConnections, and AWS Lambda optimization
  • AWS Aurora Read-Write Split — Setting up dual DataSources with Aurora reader and writer endpoints, including Ebean secondary datasource routing
  • Connection Validation Best Practices — Why Connection.isValid() is the recommended default and when (rarely) explicit heartbeatSql is needed

Verification

  1. Start the application (or run mvn test -pl <your-module>).

  2. Look for log output similar to:

    INFO  o.a.datasource.pool.ConnectionPool - DataSourcePool [db] autoCommit[false] min[1] max[5]
    INFO  io.ebean.internal.DefaultContainer - DatabasePlatform name:db platform:postgres
    
  3. If you see DataSourcePool and DatabasePlatform log lines, Ebean is connected and the database bean is wired correctly.


Troubleshooting

SymptomLikely causeFix
ClassNotFoundException: org.postgresql.DriverPostgreSQL JDBC driver missingAdd org.postgresql:postgresql dependency (see Step 1 guide)
Cannot connect to database at startupDB unreachable but skipDataSourceCheck is falseSet .skipDataSourceCheck(true)
Ebean enhancement warnings in logsebean-maven-plugin not configuredComplete Step 1 guide
NullPointerException reading config keyConfig key not definedAdd the key to application.yml or environment

The test container setup (Step 2) should already be complete and passing before this step. See add-ebean-postgres-test-container.md.