1. Overview

The Observable pattern is the foundation of Codion’s reactive architecture. It provides a unified approach to change notification throughout the framework, from UI components to domain models. At its core is a single primitive from which all reactive types derive:

  • Observer - The root reactive interface that manages listeners and consumers

  • Observable - Extends Observer, adding a value accessor

  • Value - Mutable observable wrapper for any object

  • State - Specialized mutable boolean state

  • Event - Push-only notification mechanism implementing Observer

2. Core Architecture

2.1. The Type Hierarchy

Observer<T>                   (root reactive primitive)
├── Observable<T>             (adds get() method)
│   └── Value<T>              (mutable observable)
├── ObservableState           (Observer<Boolean>)
│   └── State                 (mutable boolean state)
└── Event<T>                  (implements Observer<T>)

This hierarchy enables type-based discovery: any type that extends or implements Observer is reactive by definition.

2.2. Observer Interface

The Observer interface is the foundation for all change notification:

public interface Observer<T> {
    // Strong references - prevent garbage collection
    boolean addListener(Runnable listener);
    boolean addConsumer(Consumer<? super T> consumer);

    // Weak references - allow garbage collection
    boolean addWeakListener(Runnable listener);
    boolean addWeakConsumer(Consumer<? super T> consumer);

    // Removal methods
    boolean removeListener(Runnable listener);
    boolean removeConsumer(Consumer<? super T> consumer);
}

Key design decisions:

  1. Two notification types:

    • Runnable listeners for simple notifications

    • Consumer<T> for data propagation

  2. Weak reference support: Prevents memory leaks in long-lived UI components

2.3. Observable Interface

Observable combines value access with observation:

public interface Observable<T> extends Observer<T> {

    @Nullable T get();

    default T getOrThrow() {
        T value = get();
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }

    Observer<T> observer();  // For read-only access
}

This separation allows exposing read-only observables while keeping mutation control private.

3. Value Implementation

3.1. Value Interface

Value is the primary mutable observable:

public interface Value<T> extends Observable<T> {
    enum Notify {
        SET,     // Notify on every set() call
        CHANGED  // Notify only when value changes
    }

    void set(@Nullable T value);

    void clear();

    void map(UnaryOperator<T> mapper);

    // Linking support
    void link(Value<T> originalValue);      // Bidirectional
    void link(Observable<T> observable);    // Unidirectional

    // Validation
    boolean addValidator(Validator<? super T> validator);
}

3.2. Nullable vs Non-Null Values

Codion provides two value types:

// Nullable - can hold null
Value<String> nullable = Value.nullable();
nullable.set(null);  // OK

// Non-null - uses null substitute
Value<String> nonNull = Value.nonNull("default");
nonNull.set(null);   // Sets to "default"
nonNull.isNull();    // Always false

3.3. Value Linking

Values can be linked for automatic synchronization:

Value<Integer> primary = Value.value(10);
Value<Integer> secondary = Value.value(0);

// Bidirectional link
secondary.link(primary);  // secondary becomes 10
primary.set(20);          // both become 20
secondary.set(30);        // both become 30

// Unidirectional link
Value<String> display = Value.value("");
Observable<String> source = getDataSource();
display.link(source);  // display follows source changes

4. State Implementation

State is optimized for boolean values:

public interface State extends ObservableState {

    void set(boolean value);

    boolean is();

    void toggle();

    // Access to underlying Value
    Value<Boolean> value();

    ObservableState observable();  // Read-only view
}

4.1. State Negation

The not() method creates an inverse view of a state:

State enabled = State.state(true);
ObservableState disabled = enabled.not();

enabled.is();   // true
disabled.is();  // false

enabled.set(false);
disabled.is();  // true

// Common UI patterns
State processing = State.state();
JButton button = Components.button()
    .enabled(processing.not())  // Disabled while processing
    .build();

// Combining with other states
State.Combination canEdit = State.and(
    loggedIn,
    processing.not(),
    hasPermission
);

4.2. State Combinations

States can be combined using boolean logic:

State canSave = State.state();
State hasChanges = State.state();
State isValid = State.state();

// AND combination
State.Combination saveEnabled = State.and(canSave, hasChanges, isValid);

// OR combination
State.Combination anyProgress = State.or(loading, saving, validating);

// Dynamic combination
State.Combination dynamic = State.combination(Conjunction.AND);
dynamic.add(condition1);
dynamic.add(condition2);
dynamic.remove(condition1);

4.3. State Groups

State groups implement radio-button behavior:

State.Group viewMode = State.group();

State listView = State.state();
State tableView = State.state();
State treeView = State.state();

viewMode.add(listView, tableView, treeView);

tableView.set(true);  // Others become false
listView.set(true);   // tableView becomes false

5. Event Implementation

Event provides push-only notifications:

public interface Event<T> extends Runnable, Consumer<T>, Observer<T> {
    void run();                    // Trigger without data
    void accept(@Nullable T data); // Trigger with data

    Observer<T> observer();        // Read-only access
}

Usage patterns:

// Simple event
Event<Void> refreshRequested = Event.event();
refreshRequested.addListener(this::refresh);
refreshRequested.run();

// Data event
Event<String> errorOccurred = Event.event();
errorOccurred.addConsumer(this::showError);
errorOccurred.accept("Connection failed");

// Both listeners and consumers are notified
Event<Integer> progress = Event.event();
progress.addListener(() -> updateProgressBar());
progress.addConsumer(percent -> setProgress(percent));
progress.accept(75);  // Both are called

6. Thread Safety

The reactive components in Codion have a carefully designed thread safety model:

6.1. Thread-Safe Components

  1. State - All operations are synchronized on an internal lock

  2. ValueCollection (ValueList, ValueSet) - All operations are synchronized on an internal lock

  3. Listener Management - Adding/removing listeners is always thread-safe across all components

6.2. NOT Thread-Safe Components

  1. Value - The basic Value implementation is NOT thread-safe for mutations

  2. Event Triggering - Calling run() or accept() should be done from a single thread

  3. Observable Access - Reading values via get() while another thread is writing is not safe

6.3. Design Rationale

The decision to keep Value non-thread-safe was deliberate:

  1. Performance - Most UI applications perform mutations on a single thread (EDT in Swing)

  2. Flexibility - AbstractValue allows custom implementations that may have their own concurrency strategies

  3. Notification Complexity - Calling listeners inside synchronized blocks risks deadlocks and performance issues

  4. Opt-in Safety - Thread safety can be added where needed without forcing the cost on all users

6.4. Exception Handling

Important: If a listener throws an exception, subsequent listeners will not be notified. Always handle exceptions within your listeners:

event.addListener(() -> {
    try {
        riskyOperation();
    } catch (Exception e) {
        LOG.error("Error in listener", e);
    }
});

7. Memory Management

7.1. Weak References

Weak listeners/consumers prevent memory leaks:

public class DetailPanel {
    private final State visible = State.state();

    public void attachToMaster(Observable<Entity> selection) {
        // Weak reference prevents this panel from keeping
        // the selection model alive if panel is discarded
        selection.addWeakConsumer(this::showDetails);
    }
}

7.2. Automatic Cleanup

Weak references are cleaned up automatically:

  1. When adding/removing listeners

  2. During notification (dead references are skipped)

  3. No explicit cleanup needed

8. Performance Characteristics

8.1. Notification Strategies

Choose the appropriate notification strategy:

// CHANGED: Only when value changes (default)
Value<Integer> counter = Value.builder()
    .nonNull(0)
    .notify(Notify.CHANGED) // This is the default
    .build();

counter.set(1);  // Notifies
counter.set(1);  // No notification

// SET: Always notify, even if value unchanged
Value<String> status = Value.builder()
    .nonNull("")
    .notify(Notify.SET)
    .build();

status.set("OK");  // Notifies
status.set("OK");  // Still notifies with SET

8.2. Listener Overhead

  • Adding/removing listeners: O(n) due to synchronization

  • Notification: O(n) where n is listener count

  • Value access: O(1) - direct field access

9. Best Practices

  1. Use appropriate abstraction:

    • State for booleans

    • Value for mutable observables

    • Event for actions

    • Observable for read-only exposure

  2. Prefer weak references for UI components to prevent memory leaks

  3. Use validators for domain constraints:

Value<Integer> age = Value.builder()
       .nonNull(0)
       .validator(a -> a >= 0 && a <= 150)
       .build();
  1. Link values instead of manual synchronization:

// Instead of:
source.addConsumer(value -> target.set(value));

// Use:
target.link(source);
  1. Expose read-only views:

public class Model {
   private final State processing = State.state();

   public ObservableState processing() {
       return processing.observable();
   }
}

10. Integration Examples

10.1. UI Component Binding

// Swing component binding
JTextField textField = new JTextField();
Value<String> model = Value.value("");

// Bidirectional binding
textField.getDocument().addDocumentListener(new DocumentAdapter() {
    protected void documentChanged() {
        model.set(textField.getText());
    }
});

model.addConsumer(textField::setText);

10.2. Model State Management

public class EntityEditModel {
    private final State modified = State.state();
    private final State valid = State.state();
    private final State.Combination canSave = State.and(modified, valid);

    private final Value<Entity> entity = Value.value();

    public EntityEditModel() {
        entity.addConsumer(e -> validateEntity());
    }

    public ObservableState canSave() {
        return canSave;
    }
}

10.3. Event-Driven Architecture

public class Application {
    private final Event<Void> shutdownRequested = Event.event();
    private final Event<Exception> errorOccurred = Event.event();

    public void initialize() {
        shutdownRequested.addListener(this::performShutdown);
        errorOccurred.addConsumer(this::logError);
        errorOccurred.addConsumer(this::notifyUser);
    }
}

11. Summary

Codion’s Observable pattern provides:

  1. Single root primitive - All reactive types derive from Observer<T>

  2. Type-safe value observation with validation

  3. Memory-safe weak references for UI components

  4. Selective thread safety - State and collections are thread-safe, basic Values are not

  5. Composable state management for complex UI logic

The threading model is designed for typical UI applications where mutations happen on a single thread (like Swing’s EDT), while still providing thread-safe options (State, ValueCollection) where concurrency is common. This pragmatic approach avoids the complexity and performance costs of full thread safety while supporting concurrent scenarios where needed.