How to Implement Transactional Property Changes with Rollback Capability in Owner

Owner provides a built-in transactional mechanism that lets you intercept property changes before they take effect and abort single changes or entire batches by throwing specific rollback exceptions.

The Owner library (matteobaccan/owner) is a Java configuration management framework that maps properties to interfaces. When you need to validate configuration changes before they are applied or ensure atomic updates across multiple properties, you can implement transactional property changes with rollback capability using the library's listener interfaces and exception types.

The Transactional Listener Architecture

Owner supports transactional semantics through two specialized listener interfaces that provide a before-change hook. Unlike standard property change listeners that fire after the value is set, these interfaces allow you to veto changes before they are committed.

TransactionalPropertyChangeListener

The TransactionalPropertyChangeListener interface in org/aeonbits/owner/event/TransactionalPropertyChangeListener.java defines the contract for intercepting individual property mutations:

public interface TransactionalPropertyChangeListener extends PropertyChangeListener {
    void beforePropertyChange(PropertyChangeEvent event) 
        throws RollbackOperationException, RollbackBatchException;
}

Owner calls beforePropertyChange before writing the new value. If the method completes normally, the change proceeds. If it throws a rollback exception, Owner aborts according to the exception type.

TransactionalReloadListener

For full configuration reloads, use TransactionalReloadListener in org/aeonbits/owner/event/TransactionalReloadListener.java:

public interface TransactionalReloadListener extends ReloadListener {
    void beforeReload(ReloadEvent event) throws RollbackBatchException;
}

This hook fires when reload() is invoked or when hot-reloading detects a file change, allowing you to validate the entire new property set before it replaces the current configuration.

Rollback Exception Types

Owner distinguishes between two levels of rollback granularity through specific exception classes in org/aeonbits/owner/event/:

RollbackOperationException

Throw RollbackOperationException to abort only the current property change while allowing the rest of a batch operation to continue. The old value remains untouched, and Owner proceeds with subsequent properties.

RollbackBatchException

Throw RollbackBatchException to abort the entire batch operation. This applies to:

  • clear() operations
  • load(...) calls
  • reload() invocations
  • Bulk property sets

When this exception is caught, Owner rolls back all changes that occurred during the current operation, restoring the configuration to its pre-operation state.

Core Implementation in PropertiesManager

The orchestration logic resides in org/aeonbits/owner/PropertiesManager.java. When you register a listener using addPropertyChangeListener(String propertyName, PropertyChangeListener listener), Owner wraps it in a PropertyChangeListenerWrapper that records whether the listener implements TransactionalPropertyChangeListener:

final boolean transactional = listener instanceof TransactionalPropertyChangeListener;
propertyChangeListeners.add(
    new PropertyChangeListenerWrapper(propertyName, listener, transactional));

During mutation operations (setProperty, removeProperty, clear, load, reload), Owner:

  1. Builds a list of PropertyChangeEvents
  2. Invokes fireBeforePropertyChange(event) (lines 79-84 in PropertiesManager)
  3. Catches RollbackOperationException to skip the single change
  4. Catches RollbackBatchException to abort and ignore() the entire operation

Practical Implementation Examples

Validating a Single Property Range

This example guards max.connections to ensure values stay between 1 and 100:

import org.aeonbits.owner.Config;
import org.aeonbits.owner.ConfigFactory;
import org.aeonbits.owner.event.TransactionalPropertyChangeListener;
import org.aeonbits.owner.event.RollbackOperationException;

public interface ServerConfig extends Config {
    @Key("max.connections")
    int maxConnections();
}

public class ConnectionLimitGuard implements TransactionalPropertyChangeListener {
    @Override
    public void beforePropertyChange(PropertyChangeEvent event) 
            throws RollbackOperationException {
        if (!"max.connections".equals(event.getPropertyName())) return;
        
        int newVal = Integer.parseInt(event.getNewValue().toString());
        if (newVal < 1 || newVal > 100) {
            throw new RollbackOperationException(
                "max.connections must be between 1 and 100");
        }
    }

    @Override 
    public void propertyChange(PropertyChangeEvent e) { /* post-change logic */ }
}

Registration and usage:

ServerConfig config = ConfigFactory.create(ServerConfig.class);
config.addPropertyChangeListener("max.connections", new ConnectionLimitGuard());

// This change is rejected; old value remains
config.setProperty("max.connections", "150"); 

Preventing Batch Clear Operations

To block clear() or bulk loads entirely:

import org.aeonbits.owner.event.RollbackBatchException;

public class ImmutableConfigGuard implements TransactionalPropertyChangeListener {
    @Override
    public void beforePropertyChange(PropertyChangeEvent event) 
            throws RollbackBatchException {
        // Throwing this aborts the entire clear() or load() operation
        throw new RollbackBatchException("Configuration is immutable");
    }

    @Override 
    public void propertyChange(PropertyChangeEvent e) { }
}

Validating Full Reloads

Use TransactionalReloadListener to validate external file changes before they are applied:

import org.aeonbits.owner.event.TransactionalReloadListener;
import org.aeonbits.owner.event.ReloadEvent;

public class EssentialKeyValidator implements TransactionalReloadListener {
    @Override
    public void beforeReload(ReloadEvent event) throws RollbackBatchException {
        if (!event.getNewProperties().containsKey("essential.key")) {
            throw new RollbackBatchException("Missing required essential.key");
        }
    }

    @Override 
    public void reloadPerformed(ReloadEvent e) { }
}

Summary

  • TransactionalPropertyChangeListener provides a beforePropertyChange hook that runs before values are written, allowing validation and veto capability.
  • RollbackOperationException aborts only the current property change while continuing with other properties in a batch.
  • RollbackBatchException aborts entire operations (clear(), load(), reload()), rolling back all changes to the pre-operation state.
  • The PropertiesManager class orchestrates these callbacks, wrapping listeners and managing the transaction flow during mutations.
  • For full configuration reloads, use TransactionalReloadListener to validate entire property sets before they replace the current configuration.

Frequently Asked Questions

What is the difference between RollbackOperationException and RollbackBatchException?

RollbackOperationException aborts only the specific property change currently being processed, leaving the old value intact and allowing the rest of a batch operation to continue. RollbackBatchException aborts the entire operation—such as clear(), load(), or reload()—and rolls back all changes that occurred during that operation, restoring the configuration to its original state.

Can I use transactional listeners with hot-reloading?

Yes. Register a TransactionalReloadListener instead of a standard ReloadListener. The beforeReload(ReloadEvent event) method fires when Owner detects a file change or when reload() is called manually. Throwing RollbackBatchException from this method prevents the new configuration from replacing the current one, effectively vetoing the hot-reload.

How do I validate only specific properties while ignoring others?

Register your TransactionalPropertyChangeListener for a specific property name using addPropertyChangeListener(String propertyName, PropertyChangeListener listener). Inside beforePropertyChange, check event.getPropertyName() to ensure you are processing the correct key. Return early for unrelated properties, or throw RollbackOperationException only when your specific validation logic fails.

Where is the transactional logic implemented in the Owner source code?

The core orchestration resides in org/aeonbits/owner/PropertiesManager.java, specifically in methods like fireBeforePropertyChange and the mutation methods (setProperty, clear, load, reload). The listener interfaces and exception types are defined in the org/aeonbits/owner/event/ package, including TransactionalPropertyChangeListener.java, TransactionalReloadListener.java, RollbackOperationException.java, and RollbackBatchException.java.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →