The change event listener shown above satisfies the requirement of invalidating cached items after external data changes. But in its current form it is evicting cache items a bit too aggressively: cached items will also be purged when updating an Item instance through the application itself. This is not only not needed (as the cached item already is the current version), but it’s even counter-productive: the superfluous cache evictions will cause additional database roundtrips, resulting in longer response times.

It is therefore necessary to distinguish between data changes performed by the application itself and external data changes. Only in the latter case the affected items should be evicted from the cache. In order to do so, you can leverage the fact that each Debezium data change event contains the id of the originating transaction. Keeping track of all transactions run by the application itself allows to trigger the cache eviction only for those items altered by external transactions.

Accounting for this change, the overall architecture looks like so:

The first thing to implement is the transaction registry, i.e. a class for the transaction book keeping:

@ApplicationScoped public class KnownTransactions { private final DefaultCacheManager cacheManager; private final Cache<Long, Boolean> applicationTransactions; public KnownTransactions() { cacheManager = new DefaultCacheManager(); cacheManager.defineConfiguration( "tx-id-cache", new ConfigurationBuilder() .expiration() .lifespan(60, TimeUnit.SECONDS) .build() ); applicationTransactions = cacheManager.getCache("tx-id-cache"); } @PreDestroy public void stopCacheManager() { cacheManager.stop(); } public void register(long txId) { applicationTransactions.put(txId, true); } public boolean isKnown(long txId) { return Boolean.TRUE.equals(applicationTransactions.get(txId)); } }

This uses the Infinispan DefaultCacheManager for creating and maintaining an in-memory cache of transaction ids encountered by the application. As data change events arrive in near-realtime, the TTL of the cache entries can be rather short (in fact, the value of one minute shown in the example is chosen very conservatively, usually events should be received within seconds).

The next step is to retrieve the current transaction id whenever a request is processed by the application and register it within KnownTransactions . This should happen once per transaction. There are multiple ways for implementing this logic; in the following a Hibernate ORM FlushEventListener is used for this purpose:

class TransactionRegistrationListener implements FlushEventListener { private volatile KnownTransactions knownTransactions; public TransactionRegistrationListener() { } @Override public void onFlush(FlushEvent event) throws HibernateException { event.getSession().getActionQueue().registerProcess( session -> { Number txId = (Number) event.getSession().createNativeQuery("SELECT txid_current()") .setFlushMode(FlushMode.MANUAL) .getSingleResult(); getKnownTransactions().register(txId.longValue()); } ); } private KnownTransactions getKnownTransactions() { KnownTransactions value = knownTransactions; if (value == null) { knownTransactions = value = CDI.current().select(KnownTransactions.class).get(); } return value; } }

As there’s no portable way to obtain the transaction id, this is done using a native SQL query. In the case of Postgres, the txid_current() function can be called for that. Hibernate ORM event listeners are not subject to dependency injection via CDI. Hence the static current() method is used to obtain a handle to the application’s CDI container and get a reference to the KnownTransactions bean.

This listener will be invoked whenever Hibernate ORM is synchronizing its persistence context with the database ("flushing"), which usually happens exactly once when the transaction is committed.

Manual Flushes The session / entity manager can also be flushed manually, in which case the txid_current() function would be invoked multiple times. That’s neglected here for the sake of simplicity. The actual code in the example repo contains a slightly extended version of this class which makes sure that the transaction id is obtained only once.

To register the flush listener with Hibernate ORM, an Integrator implementation must be created and declared in the META-INF/services/org.hibernate.integrator.spi.Integrator file:

public class TransactionRegistrationIntegrator implements Integrator { @Override public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { serviceRegistry.getService(EventListenerRegistry.class) .appendListeners(EventType.FLUSH, new TransactionRegistrationListener()); } @Override public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { } }

io.debezium.examples.cacheinvalidation.persistence.TransactionRegistrationIntegrator

During bootstrap, Hibernate ORM will detect the integrator class (by means of the Java service loader), invoke its integrate() method which in turn will register the listener class for the FLUSH event.

The last step is to exclude any events stemming from transactions run by the application itself in the database change event handler:

@ApplicationScoped public class DatabaseChangeEventListener { // ... @Inject private KnownTransactions knownTransactions; private void handleDbChangeEvent(SourceRecord record) { if (record.topic().equals("dbserver1.public.item")) { Long itemId = ((Struct) record.key()).getInt64("id"); Struct payload = (Struct) record.value(); Operation op = Operation.forCode(payload.getString("op")); Long txId = ((Struct) payload.get("source")).getInt64("txId"); if (!knownTransactions.isKnown(txId) && (op == Operation.UPDATE || op == Operation.DELETE)) { emf.getCache().evict(Item.class, itemId); } } } }

And with that, you got all the pieces in place: cached Item s will only be evicted after external data changes, but not after changes done by the application itself. To confirm, you can invoke the example’s items resource using curl:

> curl -H "Content-Type: application/json" \ -X PUT \ --data '{ "description" : "North by Northwest", "price" : 20.99}' \ http://localhost:8080/cache-invalidation/rest/items/10003