This section provides detailed technical exploration of Codion’s core architectural components and patterns.

1. Architecture Deep Dive: Observable Pattern

1.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. The pattern consists of four core abstractions:

  • Observer - Manages listeners and consumers for change notifications

  • Observable - Combines a value accessor with change observation

  • Value - Mutable observable wrapper for any object

  • State - Specialized boolean observable with null-to-false coercion

  • Event - Push-only notification mechanism

1.2. Core Architecture

1.2.1. 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

1.2.2. 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.

1.3. Value Implementation

1.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);
}

1.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

1.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

1.4. State Implementation

State is optimized for boolean values:

public interface State extends ObservableState, Value<Boolean> {
    @NonNull Boolean get();  // Never null

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

1.4.1. State Negation

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

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

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

enabled.set(false);
disabled.get();  // 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
);

1.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);

1.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

1.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

1.6. Thread Safety

The observable implementations use synchronization for thread safety:

  1. Listener Management: Adding/removing listeners is synchronized

  2. Value Updates: Setting values is atomic

  3. Notification Order: Listeners are notified in registration order

  4. Exception Handling: Unhandled exceptions in listeners prevent further notifications

Example from DefaultEvent:

private void notifyListeners() {
    synchronized (listeners) {
        for (Runnable listener : listeners) {
            listener.run();  // Exception here stops the loop
        }
    }
}

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);
    }
});

1.7. Memory Management

1.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);
    }
}

1.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

1.8. Performance Characteristics

1.8.1. Notification Strategies

Choose the appropriate notification strategy:

// CHANGED: Only when value changes (default)
Value<Integer> counter = Value.builder()
    .nonNull(0)
    .build();  // Uses CHANGED by default

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

1.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

1.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();
   }
}

1.10. Integration Examples

1.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);

1.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;
    }
}

1.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);
    }
}

1.11. Summary

Codion’s Observable pattern provides:

  1. Unified change notification across the framework

  2. Type-safe value observation with validation

  3. Memory-safe weak references for UI components

  4. Thread-safe implementation for concurrent access

  5. Composable state management for complex UI logic

This pattern is fundamental to Codion’s reactive architecture, enabling automatic UI updates, clean separation of concerns, and maintainable application state management.