Skip to main content
Version: 1.11.2

Performance

Storm is designed to add minimal overhead on top of JDBC. In most applications, the bottleneck is the database itself, not the ORM layer. Still, understanding how Storm processes queries, caches compiled templates, and manages entity state helps you make informed decisions about configuration and optimization.

This page covers Storm's internal performance mechanisms, the configuration properties that control them, and the JMX metrics you can use to monitor behavior in production.


Query Execution Model

When you execute a query through Storm, the framework performs these steps:

1. Template Compilation     Parse the query template, resolve entity mappings,
and generate the SQL string with ? placeholders.

2. Cache Lookup Check the template cache for a previously compiled
result with the same shape.

3. Parameter Binding Bind runtime values to the compiled SQL template.

4. JDBC Execution Send the PreparedStatement to the database via JDBC.

5. Result Mapping Map result set rows to record instances.

Steps 1 and 2 are where Storm's compilation cache provides its largest performance benefit. Steps 4 and 5 are dominated by database I/O and are largely outside the framework's control.


Template Compilation Cache

The compilation cache is Storm's most significant performance optimization. SQL template compilation involves parsing the template structure, resolving entity metadata, generating column lists, and building the final SQL string. This work is substantial, and the compilation cache avoids repeating it.

How It Works

Each unique template shape (the combination of entity types, column selections, and query structure) produces a compiled result that is stored in a bounded LRU cache. When the same template shape is requested again, the cached result is reused and only the runtime parameter binding step is repeated.

The performance difference is significant: a cache hit typically completes in single-digit microseconds, while a cache miss (full compilation) can take tens to hundreds of microseconds depending on entity complexity.

First request (cache miss):    ~100-500 us    Full compilation
Subsequent requests (cache hit): ~1-10 us Reuse compiled result

Configuration

The cache size is configured via the storm.template_cache.size property:

PropertyDefaultDescription
storm.template_cache.size2048Maximum number of compiled templates to cache. Set to 0 to disable caching.

With Spring Boot, use the storm.template-cache.size property in application.yml:

storm:
template-cache:
size: 4096

Or configure programmatically:

StormConfig config = StormConfig.of(Map.of(
TEMPLATE_CACHE_SIZE, "4096"
));
ORMTemplate orm = ORMTemplate.of(dataSource, config);

For most applications, the default of 2048 is sufficient. If you have a large number of distinct query shapes (hundreds of different entity types or complex dynamic queries), consider increasing it. Monitor the hit ratio via JMX to determine if the cache is sized appropriately.


Entity Cache

Storm maintains a transaction-scoped entity cache that serves multiple purposes: avoiding redundant database round-trips, preserving object identity within a transaction, and providing the baseline for dirty checking.

Transaction Scope

The entity cache is created when a transaction begins and discarded when it commits or rolls back. There is no second-level or cross-transaction cache. This design avoids cache coherency problems and aligns with standard transaction isolation semantics.

Isolation-Level Awareness

The cache behavior adapts to your transaction isolation level:

Isolation LevelCache Behavior
READ_UNCOMMITTEDObservation is disabled by default. All entities are treated as dirty.
READ_COMMITTEDObservation is enabled. Cache serves dirty checking.
REPEATABLE_READFull caching. Returning cached instances matches database guarantees.
SERIALIZABLEFull caching. Same as REPEATABLE_READ.

Cache Retention

The storm.entity_cache.retention property controls how long cached entity state is retained:

ValueDescription
DEFAULTRetained for the duration of the transaction. Higher memory usage, better dirty-check hit rate.
LIGHTMay be cleaned up when the application no longer holds a reference. Lower memory usage, but may cause dirty-check cache misses.
storm:
entity-cache:
retention: LIGHT

Hit and Miss Patterns

A cache hit occurs when Storm finds a previously observed entity by primary key. This means the entity was already read in the current transaction and can be returned immediately (or used as the dirty-check baseline) without a database round-trip.

A cache miss occurs when the entity is not in the cache. This results in a database query and the entity being stored in the cache for future use.

For dirty checking specifically, a miss means no baseline is available and Storm falls back to a full-row update (all columns are included regardless of what changed).


Dirty Checking Costs

When dirty checking is enabled (via @DynamicUpdate or the storm.update.default_mode property), Storm compares entity state before generating UPDATE statements. The cost of this comparison depends on the strategy used:

INSTANCE vs VALUE Comparison

StrategyHow It WorksPerformanceTrade-off
INSTANCECompares field references using == (identity).Very fast; no value inspection.Treats structurally equal but different instances as dirty.
VALUECompares field values using equals().Depends on field types and equals() cost.More precise; only truly changed fields are dirty.

The default strategy is INSTANCE, which is fast and sufficient for most applications. If you construct entities by copying with modifications, INSTANCE will detect the change because the field references differ, even if the values are the same. Use VALUE when precision is more important than speed (for example, when equals() is cheap and unnecessary updates are expensive).

When FIELD Mode Helps

With UpdateMode.FIELD, Storm generates UPDATE statements that include only the dirty columns. This reduces write amplification and lock scope in the database. However, it introduces additional overhead:

  • Shape diversity: Each unique combination of dirty columns produces a distinct SQL shape. These shapes are cached, but too many shapes can reduce cache effectiveness.
  • Shape limit: The storm.update.max_shapes property (default: 5) limits the number of shapes per entity type. Beyond this limit, Storm falls back to full-row updates to preserve batching efficiency.
storm:
update:
default-mode: FIELD
dirty-check: VALUE
max-shapes: 10

Configuration Properties

PropertyDefaultDescription
storm.update.default_modeENTITYDefault update mode: OFF, ENTITY, or FIELD.
storm.update.dirty_checkINSTANCEDefault dirty check strategy: INSTANCE or VALUE.
storm.update.max_shapes5Maximum distinct UPDATE shapes per entity type before falling back to full-row updates.

Batch Operations

Batch operations group multiple SQL statements into a single JDBC round-trip. Storm automatically uses JDBC batching when you pass collections to insert, update, remove, or upsert.

Performance Characteristics

Batching reduces the number of network round-trips from N (one per entity) to 1 (or a few, depending on batch size). The performance improvement depends on network latency and database server efficiency:

  • Low-latency connections (same host or same datacenter): 2-5x improvement.
  • High-latency connections (cross-region): 10-100x improvement.

Streaming with Batch Size

For large data sets that do not fit in memory, use the streaming batch methods:

// Insert a stream of entities in batches of 1000.
orm.entity(User::class).insert(userStream, batchSize = 1000)

The batch size controls the trade-off between memory usage and database efficiency. Larger batches use more memory but reduce the number of round-trips. A batch size of 100-1000 is a good starting point for most applications.


Connection Management

Storm does not manage connections or connection pools. It receives a DataSource from your application and acquires connections through it. This means connection pooling is your responsibility.

HikariCP is the recommended connection pool for Storm applications. It is the default for Spring Boot applications.

spring:
datasource:
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000

Key sizing considerations:

  • maximum-pool-size: Should match your application's concurrency level. A common formula is connections = (2 * CPU cores) + disk spindles. For most applications, 10-20 is sufficient.
  • minimum-idle: Set equal to maximum-pool-size for fixed-size pools, or lower for variable workloads.
  • connection-timeout: How long a thread waits for a connection before throwing an exception. Set this lower than your application's request timeout.

JMX Metrics

Storm registers three MXBeans that provide runtime visibility into template compilation, entity caching, and dirty checking. These metrics are available through any JMX client (JConsole, VisualVM, Prometheus JMX exporter, etc.).

Template Metrics

MBean name: st.orm:type=TemplateMetrics

AttributeTypeDescription
RequestslongTotal number of template requests.
HitslongNumber of cache hits.
MisseslongNumber of cache misses.
HitRatioPercentlongHit ratio as a percentage (0-100).
AvgRequestMicroslongAverage request duration in microseconds.
MaxRequestMicroslongMaximum request duration in microseconds.
AvgHitMicroslongAverage cache hit duration in microseconds.
MaxHitMicroslongMaximum cache hit duration in microseconds.
AvgMissMicroslongAverage cache miss duration in microseconds.
MaxMissMicroslongMaximum cache miss duration in microseconds.
TemplateCacheSizeintConfigured cache size.

Operation: reset() clears all counters.

What to look for:

  • A HitRatioPercent below 90% suggests the cache is too small or the application has many distinct query shapes. Consider increasing storm.template_cache.size.
  • A large gap between AvgHitMicros and AvgMissMicros confirms that caching is providing a significant benefit.

Entity Cache Metrics

MBean name: st.orm:type=EntityCacheMetrics

AttributeTypeDescription
GetslongTotal number of get() calls.
GetHitslongCache hits (entity found in cache).
GetMisseslongCache misses (entity not cached).
GetHitRatioPercentlongGet hit ratio as a percentage (0-100).
InternslongTotal number of intern() calls (storing entities).
InternHitslongIntern hits (reused an existing canonical instance).
InternMisseslongIntern misses (stored a new instance).
InternHitRatioPercentlongIntern hit ratio as a percentage (0-100).
RemovalslongEntries removed due to entity mutations.
ClearslongFull cache clears (transaction commit/rollback).
EvictionslongEntries cleaned up after garbage collection.
RetentionPerEntityMap<String, String>Effective cache retention mode per entity type.

Operation: reset() clears all counters.

What to look for:

  • High Evictions with LIGHT retention suggests the JVM is under memory pressure. Consider switching to DEFAULT retention or increasing heap size.
  • High GetHitRatioPercent indicates the cache is working effectively for identity preservation and query optimization.

Dirty Check Metrics

MBean name: st.orm:type=DirtyCheckMetrics

AttributeTypeDescription
CheckslongTotal number of dirty checks performed.
CleanlongChecks that found the entity clean (update skipped).
DirtylongChecks that found the entity dirty (update triggered).
CleanRatioPercentlongPercentage of checks where the update was skipped.
IdentityMatcheslongChecks resolved by identity comparison (cached == entity).
CacheMisseslongChecks where no cached baseline was available (fallback to full update).
EntityModeCheckslongChecks using ENTITY update mode.
FieldModeCheckslongChecks using FIELD update mode.
InstanceStrategyCheckslongChecks using INSTANCE dirty check strategy.
ValueStrategyCheckslongChecks using VALUE dirty check strategy.
FieldComparisonslongTotal individual field comparisons across all checks.
FieldCleanlongField comparisons where the field was clean.
FieldDirtylongField comparisons where the field was dirty.
EntityTypeslongNumber of distinct entity types that have generated UPDATE shapes.
ShapeslongTotal number of distinct UPDATE statement shapes.
ShapesPerEntityMap<String, Long>Number of shapes per entity type.
UpdateModePerEntityMap<String, String>Effective update mode per entity type.
DirtyCheckPerEntityMap<String, String>Effective dirty check strategy per entity type.
MaxShapesPerEntityMap<String, Integer>Configured maximum shapes per entity type.

Operation: reset() clears all counters.

What to look for:

  • A high CleanRatioPercent means many updates are being skipped because the entity has not changed. This is the primary benefit of dirty checking.
  • CacheMisses indicates how often a dirty check falls back to a full update because no baseline was available. High values suggest entities are being updated without being read first in the same transaction.
  • ShapesPerEntity approaching MaxShapesPerEntity indicates that FIELD mode is generating many distinct column combinations. Consider raising storm.update.max_shapes or switching to ENTITY mode for that entity type.