Observable Architecture: The Foundation of Reactive UIs
How Codion eliminates manual state management through comprehensive observability
The Observable Revolution
Modern UI frameworks struggle with state management complexity. Components need to stay synchronized with data changes, business logic needs to coordinate across layers, and developers end up writing boilerplate event handling code that’s both error-prone and hard to maintain.
Codion takes a different approach: everything is observable. From the smallest UI value to complex business state, the framework provides a unified observable architecture that eliminates manual state management and enables reactive programming. Learn how this fits into Codion’s overall philosophy →
The Observable Foundation
Value: Observable Data Holders
At the heart of Codion’s reactive architecture is Value<T>
- a container for data that notifies listeners when changes occur:
// From Llemmy demo - chat prompt handling
private final Value<String> prompt = Value.builder()
.nonNull("")
// Update the promptEmpty state each time the value changes
.consumer(value -> promptEmpty.set(value.trim().isEmpty()))
.build();
Values can be nullable or non-null, and support automatic transformations:
// From World demo - calculated values
private final Value<Double> averageCityPopulation = Value.nullable();
// Observable access for UI binding
public Observable<Double> averageCityPopulation() {
return averageCityPopulation.observable();
}
Values automatically notify listeners of changes, enabling reactive UI updates without manual event handling.
State: Boolean Observables
State
objects represent boolean conditions that change over time:
// From SDKBOY demo - application state management
private final State promptEmpty = State.state(true);
private final State attachmentsEmpty = State.state(true);
private final State processing = State.state();
private final State error = State.state();
States can be combined using logical operations:
// From Llemmy demo - composite state calculation
private final ObservableState ready =
and(processing.not(), and(promptEmpty, attachmentsEmpty).not());
This declarative approach eliminates complex conditional logic - you describe the relationships once, and the framework maintains them automatically.
Reactive UI Binding
Direct Component Binding
UI components bind directly to observable values, eliminating manual synchronization:
// From SDKBOY demo - text field bound to observable filter
filter = stringField(versionModel.filter())
.hint("Filter...")
.lowerCase(true)
.selectAllOnFocusGained(true)
.transferFocusOnEnter(true)
.enabled(installTask.active.not()) // Reactive state binding
.build();
The text field automatically:
- Updates when the model’s filter value changes
- Notifies the model when the user types
- Enables/disables based on installation state
- No manual event handlers required
Checkbox State Binding
Checkboxes bind directly to State
objects:
// From SDKBOY demo - multiple filter checkboxes
installedOnly = checkBox(versionModel.installedOnly())
.text("Installed")
.mnemonic('N')
.focusable(false)
.enabled(installTask.active.not())
.build();
downloadedOnly = checkBox(versionModel.downloadedOnly())
.text("Downloaded")
.mnemonic('A')
.focusable(false)
.enabled(installTask.active.not())
.build();
Each checkbox automatically reflects and updates its corresponding model state. The framework handles all the synchronization.
Control State Management
Actions and controls respond to observable state changes:
// From SDKBOY demo - context-sensitive controls
this.installControl = Control.builder()
.command(this::install)
.enabled(and(
versionModel.tableModel().selection().empty().not(),
versionModel.selectedInstalled().not()))
.build();
this.uninstallControl = Control.builder()
.command(this::uninstall)
.enabled(and(
versionModel.tableModel().selection().empty().not(),
versionModel.selectedInstalled()))
.build();
Controls automatically enable/disable based on selection state and installation status. No manual button state management required.
Observable Entity Models
Entity Value Binding
Entity models expose observable values for each attribute:
// From Chinook demo - invoice address binding
public InvoiceEditModel(EntityConnectionProvider connectionProvider) {
super(Invoice.TYPE, connectionProvider);
// Populate invoice address fields when customer is edited
editor().value(Invoice.CUSTOMER_FK).edited().addConsumer(this::setAddress);
}
When the customer foreign key is edited, the address fields automatically update. The observable architecture ensures all dependent values stay synchronized.
Cross-Entity Coordination
Complex business logic coordinates through observable patterns:
// From World demo - country capital filtering
@Override
protected void configureComboBoxModel(ForeignKey foreignKey, EntityComboBoxModel comboBoxModel) {
if (foreignKey.equals(Country.CAPITAL_FK)) {
//only show cities for currently selected country
editor().addConsumer(country ->
comboBoxModel.filter().predicate().set(city ->
country != null && Objects.equals(city.get(City.COUNTRY_FK), country)));
}
}
The capital city dropdown automatically filters based on the selected country. As the country changes, the city list updates reactively.
Master-Detail Observables
Selection Coordination
Master-detail relationships coordinate through observable selection models:
// From SDKBOY demo - candidate selection drives version display
candidateModel.tableModel().selection().item().addConsumer(this::onCandidateChanged);
private void onCandidateSelected() {
tableModel.items().refresh(_ -> {
if (tableModel.selection().empty().get()) {
tableModel.selection().indexes().increment();
}
});
}
When a candidate is selected in the master table, the detail table automatically refreshes to show the relevant versions.
Cascading Updates
Observable models support cascading updates through the relationship hierarchy:
// From CountryEditModel - calculated values update automatically
editor().addConsumer(country ->
averageCityPopulation.set(averageCityPopulation(country)));
When the country entity changes, derived calculations automatically update, and any UI components bound to those calculations refresh accordingly.
Background Processing Integration
Progress Tracking
Long-running operations integrate seamlessly with the observable architecture:
// From Llemmy demo - chat processing state
private final Value<LocalDateTime> started = Value.nullable();
private final Value<Duration> elapsed = Value.nonNull(ZERO);
private final TaskScheduler elapsedUpdater =
TaskScheduler.builder(this::updateElapsed)
.interval(1, TimeUnit.SECONDS)
.build();
Processing state automatically updates UI indicators:
// From SDKBOY demo - progress bar binding
installProgress = progressBar()
.stringPainted(true)
.build();
// Progress updates automatically reflect in UI
ProgressWorker.builder(installTask)
.onStarted(installTask::started)
.onProgress(installTask::progress)
.onPublish(installTask::publish)
.onDone(installTask::done)
.execute();
State Coordination
Complex state machines coordinate through observable patterns:
// From SDKBOY demo - installation state management
installTask.active.addConsumer(this::onInstallActiveChanged);
installTask.downloading.addConsumer(this::onDownloadingChanged);
private void onInstallActiveChanged(boolean active) {
if (active) {
southPanel.remove(refreshProgress);
southPanel.add(installingPanel, SOUTH);
} else {
southPanel.remove(installingPanel);
southPanel.add(refreshProgress, SOUTH);
}
southPanel.revalidate();
}
UI panels automatically reconfigure based on processing state. The observable architecture handles all the coordination.
Filtering and Search Integration
Reactive Filtering
Table filtering happens through observable predicates:
// From SDKBOY demo - multiple filter coordination
private final Value<String> filter = Value.builder()
.<String>nullable()
.listener(this::onFilterChanged)
.build();
private final State installedOnly = State.builder()
.listener(this::onFilterChanged)
.build();
private void onFilterChanged() {
tableModel.items().filter();
tableModel.selection().indexes().clear();
tableModel.selection().indexes().increment();
}
Multiple filter criteria combine automatically:
// From SDKBOY demo - complex filter logic
private final class VersionVisible implements Predicate<VersionRow> {
@Override
public boolean test(VersionRow versionRow) {
if (installedOnly.get() && !candidateVersion.installed()) {
return false;
}
if (downloadedOnly.get() && !candidateVersion.available()) {
return false;
}
if (usedOnly.get() && !versionRow.used) {
return false;
}
if (filter.isNull()) {
return true;
}
// Text filtering logic...
}
}
The filter predicate automatically reevaluates when any observable filter criteria changes.
Event Propagation
Decoupled Communication
Observable events enable decoupled component communication:
// From Llemmy demo - document selection events
private final Event<List<Document>> documentsSelected = Event.event();
// Components listen for events without tight coupling
documentsSelected.addConsumer(this::onDocumentsSelected);
Events provide clean separation between UI layers and business logic.
Performance Optimizations
Lazy Evaluation
Observable chains support lazy evaluation to minimize unnecessary computations:
// From CountryEditModel - expensive calculations only when needed
private Double averageCityPopulation(Entity country) {
return country == null ? null :
connection().execute(Country.AVERAGE_CITY_POPULATION, country.get(Country.CODE));
}
The calculation only executes when the country actually changes, not on every UI update.
Batched Updates
Observable notifications can be batched to prevent UI thrashing:
// From SDKBOY demo - coordinated refresh
public void refresh() {
VersionRow selected = versionModel.selected();
candidateModel.tableModel.items().refresh(_ ->
versionModel.tableModel.items().refresh(_ ->
versionModel.tableModel.selection().item().set(selected)));
}
Multiple table refreshes coordinate to maintain selection state efficiently.
Benefits of Observable Architecture
Eliminates Boilerplate
Traditional UI frameworks require extensive event handling code:
// Traditional approach - manual synchronization
textField.addDocumentListener(new DocumentListener() {
public void changedUpdate(DocumentEvent e) { updateFilter(); }
public void removeUpdate(DocumentEvent e) { updateFilter(); }
public void insertUpdate(DocumentEvent e) { updateFilter(); }
});
checkBox.addActionListener(e -> {
boolean selected = checkBox.isSelected();
updateFilterState(selected);
refreshTable();
updateButtonStates();
});
Codion’s observable approach eliminates this boilerplate:
// Codion approach - declarative binding
JTextField textField = stringField(model.filter()).build();
JCheckBox checkBox = checkBox(model.installedOnly()).build();
The framework handles all synchronization automatically.
Prevents Inconsistent State
Observable architecture prevents common state synchronization bugs:
- UI components can’t get out of sync with model data
- Derived values automatically update when source data changes
- State combinations are computed declaratively, not imperatively
- Event ordering issues are eliminated through reactive updates
Enables Declarative Programming
Instead of describing how to manage state changes, you declare what the relationships should be:
// Declarative state relationships
State canSave = and(hasChanges, isValid, notProcessing);
State canExport = and(hasData, exportEnabled);
State ready = and(connected, initialized, errorFree);
The framework maintains these relationships automatically as underlying conditions change.
Simplifies Testing
Observable models are easy to test because state changes are predictable:
// Test observable behavior directly
model.filter().set("test");
assertTrue(model.hasResults().get());
model.processing().set(true);
assertFalse(model.ready().get());
No complex UI mocking or event simulation required.
Advanced Patterns
State Machines
Complex workflows coordinate through observable state machines:
// From Llemmy demo - chat processing state machine
State ready = and(processing.not(), and(promptEmpty, attachmentsEmpty).not());
State error = State.state();
State processing = State.state();
// State transitions coordinate UI behavior automatically
Computed Properties
Derived values update automatically based on observable dependencies:
// From CountryEditModel - computed from entity changes
editor().addConsumer(country ->
averageCityPopulation.set(averageCityPopulation(country)));
Event Sourcing
Observable events can implement event sourcing patterns:
// Events maintain audit trails automatically
editor().value(Customer.NAME).addConsumer(newValue ->
auditLog.record("Customer name changed", newValue));
Conclusion: Reactive by Design
Codion’s observable architecture represents a fundamental shift from imperative to declarative UI programming. Instead of manually coordinating state changes through complex event handling, you declare the relationships you want, and the framework maintains them automatically.
This approach:
- Eliminates boilerplate - No manual event handlers or state synchronization
- Prevents bugs - Impossible to have inconsistent state across components
- Enables composition - Complex behaviors emerge from simple observable relationships
- Simplifies maintenance - Business logic is declarative and self-documenting
- Supports testing - Observable patterns are inherently testable
The observable foundation isn’t just a feature of Codion - it’s the architectural principle that makes everything else possible. From simple form fields to complex master-detail relationships to background processing coordination, observability provides the reactive substrate that eliminates the complexity explosions common in traditional UI frameworks.
Every Value, every State, every Event in a Codion application contributes to a unified reactive architecture where changes flow naturally from source to dependent components. The result is applications that feel alive and responsive, with UI that automatically reflects the current state of the business domain.
For developers interested in exploring Codion’s observable patterns, examine the progression from simple observable values in the Petclinic demo to complex state coordination in SDKBOY to AI integration in Llemmy. Each demo builds on the same observable foundation while showing increasingly sophisticated reactive patterns.