True Cache: in-memory reads for Oracle AI Database

Oracle True Cache is a mostly diskless, read-only cache and replica that stays synchronized through redo apply: Only frequently accessed blocks reside in memory. With True Cache, read-heavy queries can be offloaded to the cache replica, improving performance on the primary.

The cache keeps the same client, database model, and SQL interface as your primary database, allowing you to serve cache-friendly reads without another system or translating cache-specific objects.

In this article, we’ll explore how True Cache works, then describe how to test it with Oracle AI Database Free and Testcontainers.

True Cache is Oracle AI Database

External caches generally developers to serialize or map database data into a separate representation..True Cache isn’t limited by external semantics, instead acting as a read-only replica of the primary database. It caches Oracle AI Database objects and data types, and applies the same database security policies.

If your application already has a relational model, SQL queries, constraints, joins, JSON, or other Oracle data types – you can add an in-memory read path without changing the way data is accessed. There’s no cache-specific object shape or serialization layer. the same SQL can run unchanged on in-memory replica.

I’ve written a True Cache integration test that demonstrates this: It creates a table with a NUMBER key and a VARCHAR2 column on the primary, inserts a row, and later runs a normal SQL SELECT against the cache to retrieve the data.

Both the primary database and replica database clients use the Oracle JDBC driver (ojdbc17). The application still decides primary or cache endpoint, but doesn’t need a secondary database client or platform to read cached data.

Mostly diskless, in-memory caching

True Cache satisfies queries from its buffer cache – a small amount of storage is required for standby redo log files, config files, and temp files. It’s a smaller persistent storage footprint than a full database replica.

The cache starts empty. On a miss, it fetches data from the primary, and it can read larger chunks while warming up. Once a block is cached, changes are applied through redo from the primary database. The frequently read part of the database ends up in memory without the application copying rows into a second schema or cache format.

The primary must run in ARCHIVELOG mode so redo can be shipped to the True Cache node. That prerequisite shows up directly in the test setup:

private static final OracleContainer PRIMARY = new OracleContainer(OracleContainer.IMAGE_NAME + ":latest")
        .withNetwork(NETWORK)
        .withNetworkAliases(PRIMARY_ALIAS)
        .withOracleSecrets(SECRETS)
        .withArchiveLog(true)
        .withForceLogging(true);

Cache freshness

With True Cache, applications must still write to the primary database.

True Cache uses “redo apply” from the primary database, and then serves that data from memory. This is very high throughput, but If read-after-write consistency is required, I recommend reading from the primary.

I would use True Cache when:

  • the workload has many more reads than writes;
  • the read path can tolerate a very small amount of redo-apply lag
  • the application benefits from keeping its Oracle SQL and data types instead of maintaining a second cache representation.

Semantically, this is really no different than another read-cache. I just want to spell this out here to be clear about redo apply.

Route writes and latest reads to the primary database. All other reads may be directed to True Cache.

One client doesn’t mean one endpoint

The True Cache application model includes two physical connections: a connection to the primary and a connection to True Cache. The application must choose its connection based on whether the operation needs to write new data, requires the latest data, or if it can use a cached read. This model is compatible with existing client drivers and programming languages.

For something like a Spring application, that could mean two DataSource instances, two JdbcClient instances, or a routing layer that selects the right one for each operation.

Oracle 26ai also supports one logical JDBC Thin connection that routes based on read-only state, plus OCI session-pool integration (Oracle connection methods). With JDBC, you can configure this with oracle.jdbc.useTrueCacheDriverConnection=true and Connection.setReadOnly(boolean).

Try it out with Testcontainers

TrueCacheContainer.java joins primary and cache database containers with a shared network in TrueCacheContainerIntegrationTest.java:

  1. The primary and cache are started on the same Testcontainers Network.
  2. The cache uses the primary’s connection string
  3. Tell the cache which Pluggable Database (PDB) service maps to which True Cache service.
  4. Give the cache the primary password file it needs during startup.
Diagram illustrating a two-container topology with primary and cache containers in a testcontainers network. It shows the OracleContainer (alias: pri-db-free) transferring data to the TrueCacheContainer (alias: tru-cc-free) via a password transfer. Includes steps for starting primary, copying password, starting cache, and configuring service.

The integration test wires those pieces together like this:

private static final Network NETWORK = Network.newNetwork();
private static final OracleContainerSecrets SECRETS = OracleContainerSecrets.withOraclePassword(OracleContainer.DEFAULT_PASSWORD);

private static final OracleContainer PRIMARY = new OracleContainer(OracleContainer.IMAGE_NAME + ":latest")
        .withStartupTimeout(Duration.ofMinutes(5))
        .withNetwork(NETWORK)
        .withNetworkAliases(PRIMARY_ALIAS)
        .withOracleSecrets(SECRETS)
        .withArchiveLog(true)
        .withForceLogging(true);
private static final TrueCacheContainer CACHE = new TrueCacheContainer()
        .withNetwork(NETWORK)
        .withNetworkAliases(CACHE_ALIAS)
        .withOracleSecrets(SECRETS)
        .withPrimaryDatabase(PRIMARY_ALIAS, OracleContainer.ORACLE_PORT, "FREE")
        .withPdbService("FREEPDB1", "FREEPDB1", "FREEPDB1_TC");

The cache endpoint has to exist before the primary can advertise the corresponding service, and service registration changes the primary’s state, so the wrapper leaves it to the test or application owner.

The test waits for a row to arrive

Once both containers are running, the test confirms if a row committed to the primary can be read through the cache:

try (Connection primaryConnection = PRIMARY.createConnection("");
     Statement statement = primaryConnection.createStatement()) {
    primaryConnection.setAutoCommit(false);
    statement.execute("CREATE TABLE true_cache_test (id NUMBER PRIMARY KEY, name VARCHAR2(30))");
    statement.execute("INSERT INTO true_cache_test VALUES (1, 'Ada')");
    primaryConnection.commit();
}

assertTrue(awaitCachedRow(), "Expected True Cache to apply the committed primary row");

awaitCachedRow() opens a connection to the cache and retries the query for up to two minutes. It catches SQL exceptions while the service is being created and redo is being applied, then waits one second before trying again. 

The test passes only after the committed row became visible through the True Cache service.

References

Leave a Reply

Discover more from andersswanson.dev

Subscribe now to keep reading and get access to the full archive.

Continue reading