Relational Database Tuning

JPA event storage engine

When using Axon Framework’s JPA event storage engine with a relational database, there are a few things worth tuning for better throughput and more predictable event-store behavior:

  • use the right indexes for the event store tables

  • keep the JPA sequence generator aligned with the framework defaults

  • size the database connection pool for pooled streaming event processors

  • remember that the JPA event store in Axon Framework 5 is aggregate-based, not DCB-based

Important indices

If your JPA provider generated the tables automatically, verify that the indexes match the access patterns of the event store. The default AggregateBasedJpaEventStorageEngine uses the following access patterns:

Normal event storage and loading
  • Table AggregateEventEntry, columns aggregateIdentifier and aggregateSequenceNumber (unique index)

  • Table AggregateEventEntry, identifier (unique index)

Token management for streaming processors
  • Table TokenEntry, columns processorName, owner, segment (unique index)

The default column lengths generated by a JPA provider may work, but they are not always optimal. For example, a UUID fits in a fixed 36 character column instead of a variable length string column.

The timestamp column stores ISO 8601 timestamps. When everything is stored in UTC, a fixed length column is usually sufficient.

It is highly recommended to store all timestamps in UTC format. In countries with daylight saving time, storing timestamps in local time may result in sorting errors for events generated around and during the timezone switch. This does not occur when UTC is used. Some servers are configured to always use UTC. Alternatively, you should configure the event store to convert timestamps to UTC before storing them.

The aggregateType and type columns store identifiers that are compared and queried often. Keep them compact, but do not sacrifice readability or compatibility with the configured message types.

Auto-increment and sequences

When using the JPA-backed event store, Axon Framework relies on the generated globalIndex to let streaming processors read events in event-store order. The generated value is assigned at insert time, but it only becomes visible when the transaction commits.

That means another process can observe events in a different order when transactions finish at different times. Axon Framework handles those gaps, but they still affect performance.

When a streaming processor reads events, it uses the global sequence to track progress. If events become visible out of order, Axon Framework tracks gaps and revisits them later. That is safe, but gap handling has a cost, so avoid creating unnecessary gaps.

The JPA event store uses the AggregateEventEntry entity and its aggregate-event-global-index-sequence generator. Keep the allocation size at 1 so the sequence stays predictable across application instances.

If you need to override the generated mapping, use a JPA orm.xml file on the classpath. The goal is to keep the sequence allocation stable, not to change the storage model.

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings version="1.0" xmlns="http://java.sun.com/xml/ns/persistence/orm">
    <mapped-superclass access="FIELD"
                       metadata-complete="false"
                       class="org.axonframework.eventsourcing.eventstore.jpa.AggregateEventEntry">
        <attributes>
            <id name="globalIndex">
                <generated-value strategy="SEQUENCE" generator="myGenerator"/>
                <sequence-generator name="myGenerator" sequence-name="mySequence" allocation-size="1"/>
            </id>
        </attributes>
    </mapped-superclass>
</entity-mappings>

It is important to specify metadata-complete="false". This indicates this file should be used to override existing annotations, instead of replacing them. For the best results, keep the event table on its own sequence and do not share it with unrelated tables.

PostgreSQL, @Lob-annotated columns, and default Hibernate mapping

PostgreSQL has two relevant mechanisms for large values: TOAST and Large Object Storage. TOAST, the "oversized attribute storage technique," compresses columns larger than 8 KB when possible and otherwise moves them out of line into a separate table. Large Object Storage uses a fixed OID column type and points to the actual data in a dedicated large object table.

That distinction matters for Axon Framework because JPA uses @Lob for several serialized columns. Those include the event payload and metadata columns, token storage, saga state, and dead-letter diagnostics.

With PostgreSQL and Hibernate, @Lob fields are mapped to the large object path depending on the Hibernate version you are using. which means the main table stores an OID instead of the actual byte array. That default is usually not what you want for Axon Framework storage. It adds indirection on reads, leaves large object rows behind when the original row changes, and makes token updates more expensive than they need to be. Where possible, prefer the normal byte array storage path and let PostgreSQL use TOAST instead of Large Object Storage.

Hibernate’s PostgreSQL LOB behavior is version-dependent, so double-check the mapping for the Hibernate version you are actually using before assuming @Lob will follow the normal BYTEA / TEXT path. Older baselines may still force OID for @Lob-annotated fields.

The practical options to handle this are:

  1. Adjust the Hibernate dialect so @Lob maps to BYTEA.

  2. Override the Hibernate mapping for the affected columns.

  3. Optionally migrate existing columns from OID to BYTEA.

The PostgreSQL Event Storage Engine is a dedicated event storage engine optimized for PostgreSQL 16, including full DCB support. Consider it as an alternative to the JPA-backed eventstore if you are using PostgreSQL.

Hibernate dialect changes

To avoid OID, enforce BYTEA by providing a custom PostgreSQL dialect.

public class ByteaEnforcedPostgresSQLDialect extends PostgreSQLDialect {

    public ByteaEnforcedPostgresSQLDialect() {
        super(DatabaseVersion.make(9, 5));
    }

    @Override
    protected String columnType(int sqlTypeCode) {
        return sqlTypeCode == SqlTypes.BLOB ? "bytea" : super.columnType(sqlTypeCode);
    }

    @Override
    protected String castType(int sqlTypeCode) {
        return sqlTypeCode == SqlTypes.BLOB ? "bytea" : super.castType(sqlTypeCode);
    }

    @Override
    public void contributeTypes(TypeContributions typeContributions,
                                ServiceRegistry serviceRegistry) {
        super.contributeTypes(typeContributions, serviceRegistry);
        JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration()
                                                             .getJdbcTypeRegistry();
        jdbcTypeRegistry.addDescriptor(Types.BLOB, BinaryJdbcType.INSTANCE);
    }
}

With the dialect in place, configure Hibernate to use it, for example through Spring:

jpa.database-platform=fully.qualified.classname.ByteaEnforcedPostgresSQLDialect

Hibernate mapping override

You can also use Hibernate metadata overrides to map affected columns to BYTEA explicitly. Add an orm.xml file on the classpath under META-INF with the required overrides.

<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" version="2.0">
    <entity class="org.axonframework.messaging.eventhandling.processing.streaming.token.store.jpa.TokenEntry">
        <attribute-override name="token">
            <column name="token" column-definition="BYTEA"></column>
        </attribute-override>
    </entity>
</entity-mappings>

OID to BYTEA column migration

If you already have Axon-specific tables using OID, migrate them to BYTEA. For the token_entry table, the following SQL is a typical approach:

ALTER TABLE token_entry ADD COLUMN token_bytea BYTEA;
UPDATE token_entry SET token_bytea = lo_get(token);
ALTER TABLE token_entry DROP COLUMN token;
ALTER TABLE token_entry RENAME COLUMN token_bytea TO token;

After the migration, the affected columns can be read directly again without going through large object indirection.

JPA and DCB

The JPA-backed event store in Axon Framework 5 is aggregate-based. If you are using Dynamic Consistency Boundaries, choose the event store or extension that supports that model instead.

That distinction matters for performance tuning as well. JPA tuning here affects the aggregate-based storage path and the token store used by streaming processors.

Connection pools

Database activity in Axon Framework is not limited to event appends. Streaming processors claim tokens, advance tokens, and process events in batches, so the pool must support sustained concurrent access.

Size the pool so event handling does not block on connection acquisition. Leave enough headroom for command handling, token updates, and any other database users in the same application.

PostgreSQL operations

Schema

The PostgresqlEventStorageEngine creates the necessary schema automatically on startup if it does not yet exist. For production deployments, managing the schema through a migration tool such as Flyway or Liquibase gives you full control over schema changes across versions.

The complete DDL is as follows:

CREATE TABLE IF NOT EXISTS events (
  global_index INT8        NOT NULL GENERATED BY DEFAULT AS IDENTITY (INCREMENT BY -1),
  timestamp    TIMESTAMPTZ NOT NULL,
  payload      BYTEA,
  metadata     JSON        NOT NULL,
  identifier   VARCHAR     NOT NULL,
  type         VARCHAR     NOT NULL,
  PRIMARY KEY (global_index)
);

CREATE TABLE IF NOT EXISTS tags (
  global_index INT8    NOT NULL,
  key          VARCHAR NOT NULL,
  value        VARCHAR NOT NULL,
  PRIMARY KEY (key, value, global_index)
);

CREATE TABLE IF NOT EXISTS consistency_tags (
  tag_hash     INT4 NOT NULL,
  global_index INT8 NOT NULL,
  PRIMARY KEY (tag_hash)
);

CREATE SEQUENCE IF NOT EXISTS events_monotonic_seq
  INCREMENT BY 1
  CACHE 1
  OWNED BY events.global_index;

CREATE INDEX IF NOT EXISTS consistency_tags_global_index_idx
  ON consistency_tags (global_index);

The events table is the main event log; payload holds the serialized event as bytes and metadata is stored as JSON. The tags table associates DCB tags with events. The consistency_tags table is a bounded conflict-detection structure used when appending events with consistency requirements; it does not grow with event volume.

Table and sequence names are fixed

Table and sequence names cannot be configured. All tables and sequences use the fixed names shown above.

Connection pool sizing

The engine holds one dedicated connection open for the lifetime of the application to receive event arrival notifications from other instances. When sizing your connection pool, add one to whatever your application otherwise requires.

Multi-instance coordination

Multiple instances sharing the same PostgreSQL database coordinate automatically. Events appended by any instance are immediately visible to all other instances with no polling delay. A new instance determines the current event position immediately on startup: it subscribes to notifications before running an initial position query, so no events can be missed between the two steps. If the monitoring connection is lost, the engine reconnects and re-establishes the current position automatically.