1. Project

1.1. Building

The Codion framework is built with Gradle and includes the Gradle Wrapper with a toolchain defined, so assuming you have cloned the repository and worked your way into the project directory you can build the framework by running the following command.

gradlew build
Note
This may take a few minutes, depending on the machine.

To install the Codion framework into your local Maven repository run the following command.

gradlew publishToMavenLocal

1.2. Running the demos

Note
The demos use an embedded in-memory database, so changes to data do not persist.

1.2.1. Local database connection

You can start by running a client from one of the demo projects (employees, chinook, petstore or world) with a local database connection.

gradlew demo-chinook:runClientLocal

1.2.2. Remote database connection

In order to run a client with a remote or http connection the remote server must be started first.

gradlew demo-server:run

To run a demo client with a remote connection use the following command.

gradlew demo-chinook:runClientRMI

You can run the Server Monitor application to see how the server is behaving, with the following command.

gradlew demo-server-monitor:run
Note
The client handles server restarts gracefully, you can try shutting down the server via the Server Monitor, play around in the client until you get a 'Connection refused' exception. After you restart the server the client simply reconnects and behaves as if nothing happened.

1.3. Code style

After having wrestled with many code formatters and never being fully satisfied with the result, I’ve wound up relying on IntelliJ for code formatting. The project has a defined code style which can be found in the .idea/codeStyles folder.

1.4. Code quality

2. Architecture

The Codion framework is based on a three tiered architecture.

  • Database layer

  • Model layer

  • UI layer

2.1. Database layer

The EntityConnection class defines the database layer. See Manual:EntityConnection

2.2. Model layer

The EntityModel class defines the model layer. See Manual:EntityModel.

2.3. UI layer

The EntityPanel class defines the UI layer. See Manual:EntityPanel.

3. Client

3.1. Features

  • Lightweight client with a simple synchronous event model

  • Provides a practically mouse free user experience

  • Graceful handling of network outages and server restarts

  • Clear separation between model and UI

  • Easy to use load testing harness provided for applications

  • UI data bindings for most common components provided by the framework

  • Implementing data bindings for new components is made simple with building blocks provided by the framework

  • The default UI layout is a simple and intuitive “waterfall” master-detail view

  • Extensive searching and filtering capabilities

  • Flexible keyboard-centric UI based on tab and split panes, detachable panels and toolbars

  • Detailed logging of client actions

3.2. Default client layout

The default master/detail panel layout.

Client UI

3.3. Architecture

3.3.1. UI

ui architecture

3.3.2. Model

model architecture

3.3.3. Assembly

EntityModel
  /**
   * Creates a SwingEntityModel based on the {@link Artist#TYPE} entity
   * with a detail model based on {@link Album#TYPE}
   * @param connectionProvider the connection provider
   */
  static SwingEntityModel artistModel(EntityConnectionProvider connectionProvider) {
    // create a default edit model
    SwingEntityEditModel artistEditModel =
            new SwingEntityEditModel(Artist.TYPE, connectionProvider);

    // create a default table model, wrapping the edit model
    SwingEntityTableModel artistTableModel =
            new SwingEntityTableModel(artistEditModel);

    // create a default model wrapping the table model
    SwingEntityModel artistModel =
            new SwingEntityModel(artistTableModel);

    // Note that this does the same as the above, that is, creates
    // a SwingEntityModel with a default edit and table model
    SwingEntityModel albumModel =
            new SwingEntityModel(Album.TYPE, connectionProvider);

    artistModel.detailModels().add(albumModel);

    return artistModel;
  }
EntityPanel
  /**
   * Creates a EntityPanel based on the {@link Artist#TYPE} entity
   * with a detail panel based on {@link Album#TYPE}
   * @param connectionProvider the connection provider
   */
  static EntityPanel artistPanel(EntityConnectionProvider connectionProvider) {
    // create the EntityModel to base the panel on (calling the above method)
    SwingEntityModel artistModel = artistModel(connectionProvider);

    // the edit model
    SwingEntityEditModel artistEditModel = artistModel.editModel();

    // the table model
    SwingEntityTableModel artistTableModel = artistModel.tableModel();

    // the album detail model
    SwingEntityModel albumModel = artistModel.detailModels().get(Album.TYPE);

    // create a EntityEditPanel instance, based on the artist edit model
    EntityEditPanel artistEditPanel = new EntityEditPanel(artistEditModel) {
      @Override
      protected void initializeUI() {
        createTextField(Artist.NAME).columns(15);
        addInputPanel(Artist.NAME);
      }
    };
    // create a EntityTablePanel instance, based on the artist table model
    EntityTablePanel artistTablePanel = new EntityTablePanel(artistTableModel);

    // create a EntityPanel instance, based on the artist model and
    // the edit and table panels from above
    EntityPanel artistPanel = new EntityPanel(artistModel, artistEditPanel, artistTablePanel);

    // create a new EntityPanel, without an edit panel and
    // with a default EntityTablePanel
    EntityPanel albumPanel = new EntityPanel(albumModel);

    artistPanel.detailPanels().add(albumPanel);

    return artistPanel;
  }

3.3.4. Full Example

Show code
package is.codion.demos.chinook.tutorial;

import is.codion.common.db.database.Database;
import is.codion.common.user.User;
import is.codion.demos.chinook.domain.ChinookImpl;
import is.codion.demos.chinook.domain.api.Chinook.Album;
import is.codion.demos.chinook.domain.api.Chinook.Artist;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.model.SwingEntityTableModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.EntityTablePanel;

/**
 * When running this make sure the chinook demo module directory is the
 * working directory, due to a relative path to a db init script
 */
public final class ClientArchitecture {

  // tag::entityModel[]

  /**
   * Creates a SwingEntityModel based on the {@link Artist#TYPE} entity
   * with a detail model based on {@link Album#TYPE}
   * @param connectionProvider the connection provider
   */
  static SwingEntityModel artistModel(EntityConnectionProvider connectionProvider) {
    // create a default edit model
    SwingEntityEditModel artistEditModel =
            new SwingEntityEditModel(Artist.TYPE, connectionProvider);

    // create a default table model, wrapping the edit model
    SwingEntityTableModel artistTableModel =
            new SwingEntityTableModel(artistEditModel);

    // create a default model wrapping the table model
    SwingEntityModel artistModel =
            new SwingEntityModel(artistTableModel);

    // Note that this does the same as the above, that is, creates
    // a SwingEntityModel with a default edit and table model
    SwingEntityModel albumModel =
            new SwingEntityModel(Album.TYPE, connectionProvider);

    artistModel.detailModels().add(albumModel);

    return artistModel;
  }
  // end::entityModel[]
  // tag::entityPanel[]

  /**
   * Creates a EntityPanel based on the {@link Artist#TYPE} entity
   * with a detail panel based on {@link Album#TYPE}
   * @param connectionProvider the connection provider
   */
  static EntityPanel artistPanel(EntityConnectionProvider connectionProvider) {
    // create the EntityModel to base the panel on (calling the above method)
    SwingEntityModel artistModel = artistModel(connectionProvider);

    // the edit model
    SwingEntityEditModel artistEditModel = artistModel.editModel();

    // the table model
    SwingEntityTableModel artistTableModel = artistModel.tableModel();

    // the album detail model
    SwingEntityModel albumModel = artistModel.detailModels().get(Album.TYPE);

    // create a EntityEditPanel instance, based on the artist edit model
    EntityEditPanel artistEditPanel = new EntityEditPanel(artistEditModel) {
      @Override
      protected void initializeUI() {
        createTextField(Artist.NAME).columns(15);
        addInputPanel(Artist.NAME);
      }
    };
    // create a EntityTablePanel instance, based on the artist table model
    EntityTablePanel artistTablePanel = new EntityTablePanel(artistTableModel);

    // create a EntityPanel instance, based on the artist model and
    // the edit and table panels from above
    EntityPanel artistPanel = new EntityPanel(artistModel, artistEditPanel, artistTablePanel);

    // create a new EntityPanel, without an edit panel and
    // with a default EntityTablePanel
    EntityPanel albumPanel = new EntityPanel(albumModel);

    artistPanel.detailPanels().add(albumPanel);

    return artistPanel;
  }
  // end::entityPanel[]

  public static void main(String[] args) {
    // Configure the database
    Database.URL.set("jdbc:h2:mem:h2db");
    Database.INIT_SCRIPTS.set("src/main/sql/create_schema.sql");

    // initialize a connection provider, this class is responsible
    // for supplying a valid connection or throwing an exception
    // in case a connection can not be established
    EntityConnectionProvider connectionProvider =
            LocalEntityConnectionProvider.builder()
                    .domain(new ChinookImpl())
                    .user(User.parse("scott:tiger"))
                    .build();

    EntityPanel artistPanel = artistPanel(connectionProvider);

    // lazy initialization
    artistPanel.initialize();

    // fetch data from the database
    artistPanel.model().tableModel().items().refresh();

    // uncomment the below line to display the panel
//    displayInDialog(null, artistPanel, "Artists");

    connectionProvider.close();
  }
}

3.4. Configuration

3.4.1. Example configuration file

codion.client.connectionType=local
codion.db.url=jdbc:h2:mem:h2db
codion.db.initScripts=classpath:create_schema.sql

3.5. Usage

4. Testing Strategy & Patterns

4.1. Overview

Codion applications follow a layered testing approach, with each layer providing specific testing capabilities:

  • Domain Testing - Validates entity definitions, relationships, and domain logic

  • Model Testing - Tests business logic in edit and table models

  • Integration Testing - Verifies end-to-end functionality with database operations

4.2. Test Configuration

4.2.1. User Configuration

The test user is configured via the codion.test.user system property:

./gradlew test -Dcodion.test.user=scott:tiger

Or in your test configuration:

private static final User UNIT_TEST_USER =
        User.parse(System.getProperty("codion.test.user", "scott:tiger"));

4.3. Domain Testing

The DomainTest base class provides comprehensive testing for entity definitions.

4.3.1. Basic Domain Test

public class ChinookTest extends DomainTest {

    public ChinookTest() {
        super(new ChinookImpl(), ChinookEntityFactory::new);
    }

    @Test
    void album() {
        test(Album.TYPE);
    }

    @Test
    void artist() {
        test(Artist.TYPE);
    }
}

The test(EntityType) method automatically verifies:

  • Entity can be instantiated

  • All attributes are correctly defined

  • Primary keys work correctly

  • Foreign key relationships are valid

  • Insert, update, delete, and select operations succeed

  • Entity validation rules are enforced

4.3.2. Custom Entity Factory

For entities with complex validation rules or required relationships, provide a custom EntityFactory:

public class ChinookEntityFactory extends DefaultEntityFactory {

    @Override
    public Entity entity(EntityType entityType) {
        if (entityType.equals(InvoiceLine.TYPE)) {
            return createInvoiceLine();
        }

        return super.entity(entityType);
    }

    private Entity createInvoiceLine() {
        Entity invoiceLine = entities.entity(InvoiceLine.TYPE);
        // Set required relationships and values
        invoiceLine.set(InvoiceLine.INVOICE_FK, randomInvoice());
        invoiceLine.set(InvoiceLine.TRACK_FK, randomTrack());
        invoiceLine.set(InvoiceLine.QUANTITY, 1);

        return invoiceLine;
    }
}

4.3.3. Testing Domain Functions

Test database functions and procedures through the domain:

@Test
void randomPlaylist() {
    EntityConnection connection = connection();
    connection.startTransaction();
    try {
        List<Entity> genres = connection.select(Select.all(Genre.TYPE)
                .limit(3)
                .build());

        Entity playlist = connection.execute(Playlist.RANDOM_PLAYLIST,
                new RandomPlaylistParameters("Test Playlist", 10, genres));

        assertNotNull(playlist);
        assertEquals(10, connection.count(where(PlaylistTrack.PLAYLIST_FK.equalTo(playlist))));
    }
    finally {
        connection.rollbackTransaction();
    }
}

4.4. Model Testing

Test business logic in your Swing models independently of the UI.

4.4.1. Edit Model Testing

package is.codion.demos.world.model;

import is.codion.common.user.User;
import is.codion.demos.world.domain.WorldImpl;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

public class CountryEditModelTest {

  private static final User UNIT_TEST_USER =
          User.parse(System.getProperty("codion.test.user", "scott:tiger"));

  @Test
  void averageCityPopulation() {
    try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
      CountryEditModel countryEditModel = new CountryEditModel(connectionProvider);
      countryEditModel.editor().set(connectionProvider.connection().selectSingle(
              Country.NAME.equalTo("Afghanistan")));
      assertEquals(583_025, countryEditModel.averageCityPopulation().get());
      countryEditModel.editor().defaults();
      assertNull(countryEditModel.averageCityPopulation().get());
    }
  }

  private static EntityConnectionProvider createConnectionProvider() {
    return LocalEntityConnectionProvider.builder()
            .domain(new WorldImpl())
            .user(UNIT_TEST_USER)
            .build();
  }
}

4.4.2. Table Model Testing

Test table model behavior and master-detail relationships:

package is.codion.demos.chinook.model;

import is.codion.common.user.User;
import is.codion.demos.chinook.domain.ChinookImpl;
import is.codion.demos.chinook.domain.api.Chinook.Album;
import is.codion.demos.chinook.domain.api.Chinook.Track;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.domain.entity.Entity;
import is.codion.swing.framework.model.SwingEntityTableModel;

import org.junit.jupiter.api.Test;

import java.util.List;

import static is.codion.framework.db.EntityConnection.Update.where;
import static org.junit.jupiter.api.Assertions.assertEquals;

public final class AlbumModelTest {

  private static final String MASTER_OF_PUPPETS = "Master Of Puppets";

  @Test
  void albumRefreshedWhenTrackRatingIsUpdated() {
    try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
      EntityConnection connection = connectionProvider.connection();
      connection.startTransaction();

      // Initialize all the tracks with an inital rating of 8
      Entity masterOfPuppets = connection.selectSingle(Album.TITLE.equalTo(MASTER_OF_PUPPETS));
      connection.update(where(Track.ALBUM_FK.equalTo(masterOfPuppets))
              .set(Track.RATING, 8)
              .build());
      // Re-select the album to get the updated rating, which is the average of the track ratings
      masterOfPuppets = connection.selectSingle(Album.TITLE.equalTo(MASTER_OF_PUPPETS));
      assertEquals(8, masterOfPuppets.get(Album.RATING));

      // Create our AlbumModel and configure the query condition
      // to populate it with only Master Of Puppets
      AlbumModel albumModel = new AlbumModel(connectionProvider);
      SwingEntityTableModel albumTableModel = albumModel.tableModel();
      albumTableModel.queryModel().condition().get(Album.TITLE).set().equalTo(MASTER_OF_PUPPETS);
      albumTableModel.items().refresh();
      assertEquals(1, albumTableModel.items().size());

      List<Entity> modifiedTracks = connection.select(Track.ALBUM_FK.equalTo(masterOfPuppets)).stream()
              .peek(track -> track.set(Track.RATING, 10))
              .toList();

      // Update the tracks using the edit model
      albumModel.detailModels().get(Track.TYPE).editModel().update(modifiedTracks);

      // Which should trigger the refresh of the album in the Album model
      // now with the new rating as the average of the track ratings
      assertEquals(10, albumTableModel.items().included().get(0).get(Album.RATING));

      connection.rollbackTransaction();
    }
  }

  private static EntityConnectionProvider createConnectionProvider() {
    return LocalEntityConnectionProvider.builder()
            .domain(new ChinookImpl())
            .user(User.parse("scott:tiger"))
            .build();
  }
}

4.5. Integration Testing

Test complete workflows across multiple entities and models.

4.5.1. Testing Report Generation

package is.codion.demos.world.model;

import is.codion.common.user.User;
import is.codion.common.value.Value;
import is.codion.demos.world.domain.WorldImpl;
import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.attribute.Attribute;
import is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.base.JRBaseField;
import org.junit.jupiter.api.Test;

import java.util.List;

import static is.codion.framework.db.EntityConnection.Select.where;
import static is.codion.framework.domain.entity.OrderBy.ascending;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public final class CountryReportDataSourceTest {

  private static final User UNIT_TEST_USER =
          User.parse(System.getProperty("codion.test.user", "scott:tiger"));

  @Test
  void iterate() throws JRException {
    try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
      Value<Integer> progressCounter = Value.nullable();
      Value<String> publishedValue = Value.nullable();
      ProgressReporter<String> progressReporter = new ProgressReporter<>() {
        @Override
        public void report(int progress) {
          progressCounter.set(progress);
        }

        @Override
        public void publish(String... chunks) {
          publishedValue.set(chunks[0]);
        }
      };

      EntityConnection connection = connectionProvider.connection();
      List<Entity> countries =
              connection.select(where(Country.NAME.in("Denmark", "Iceland"))
                      .orderBy(ascending(Country.NAME))
                      .build());
      CountryReportDataSource countryReportDataSource = new CountryReportDataSource(countries.iterator(), connection, progressReporter);
      assertThrows(IllegalStateException.class, countryReportDataSource::cityDataSource);

      countryReportDataSource.next();
      assertEquals("Denmark", countryReportDataSource.getFieldValue(field(Country.NAME)));
      assertEquals("Europe", countryReportDataSource.getFieldValue(field(Country.CONTINENT)));
      assertEquals("Nordic Countries", countryReportDataSource.getFieldValue(field(Country.REGION)));
      assertEquals(43094d, countryReportDataSource.getFieldValue(field(Country.SURFACEAREA)));
      assertEquals(5330000, countryReportDataSource.getFieldValue(field(Country.POPULATION)));
      assertThrows(JRException.class, () -> countryReportDataSource.getFieldValue(field(City.LOCATION)));

      JRDataSource denmarkCityDataSource = countryReportDataSource.cityDataSource();
      denmarkCityDataSource.next();
      assertEquals("København", denmarkCityDataSource.getFieldValue(field(City.NAME)));
      assertEquals(495699, denmarkCityDataSource.getFieldValue(field(City.POPULATION)));
      assertThrows(JRException.class, () -> denmarkCityDataSource.getFieldValue(field(Country.REGION)));
      denmarkCityDataSource.next();
      assertEquals("Århus", denmarkCityDataSource.getFieldValue(field(City.NAME)));

      countryReportDataSource.next();
      assertEquals("Iceland", countryReportDataSource.getFieldValue(field(Country.NAME)));

      JRDataSource icelandCityDataSource = countryReportDataSource.cityDataSource();
      icelandCityDataSource.next();
      assertEquals("Reykjavík", icelandCityDataSource.getFieldValue(field(City.NAME)));

      assertEquals(2, progressCounter.get());
      assertEquals("Iceland", publishedValue.get());
    }
  }

  private static EntityConnectionProvider createConnectionProvider() {
    return LocalEntityConnectionProvider.builder()
            .domain(new WorldImpl())
            .user(UNIT_TEST_USER)
            .build();
  }

  private static JRField field(Attribute<?> attribute) {
    return new TestField(attribute.name());
  }

  private static final class TestField extends JRBaseField {

    private TestField(String name) {
      this.name = name;
    }
  }
}

4.6. Test Utilities

4.6.1. Connection Provider Setup

The LocalEntityConnectionProvider.Builder requires the codion.db.url system property to be set:

-Dcodion.db.url=jdbc:h2:mem:h2db

Create connection providers for testing:

private static EntityConnectionProvider createConnectionProvider() {
    return LocalEntityConnectionProvider.builder()
            .domain(new WorldImpl())
            .user(UNIT_TEST_USER)
            .build();
}

Alternatively, you can provide your own Database instance:

private static EntityConnectionProvider createConnectionProvider() {
    Database database = H2DatabaseFactory.createDatabase("jdbc:h2:mem:testdb");

    return LocalEntityConnectionProvider.builder()
            .domain(new WorldImpl())
            .database(database)
            .user(UNIT_TEST_USER)
            .build();
}

4.6.2. Transaction Management

Always use transactions for data modification tests:

@Test
void updateTest() {
    EntityConnection connection = connection();
    connection.startTransaction();
    try {
        // Perform updates
        Entity entity = connection.selectSingle(Country.CODE2.equalTo("IS"));
        entity.set(Country.POPULATION, 400_000);
        connection.update(entity);

        // Verify changes
        Entity updated = connection.selectSingle(Country.CODE2.equalTo("IS"));
        assertEquals(400_000, updated.get(Country.POPULATION));
    }
    finally {
        connection.rollbackTransaction();
    }
}

4.6.3. Test Data Builders

Create fluent builders for complex test data:

public class TestDataBuilder {

    public static Entity.Builder customer() {
        return entities.entity(Customer.TYPE)
                .with(Customer.FIRST_NAME, "Test")
                .with(Customer.LAST_NAME, "Customer")
                .with(Customer.EMAIL, "test@example.com");
    }

    public static Entity.Builder invoice(Entity customer) {
        return entities.entity(Invoice.TYPE)
                .with(Invoice.CUSTOMER_FK, customer)
                .with(Invoice.INVOICE_DATE, LocalDate.now())
                .with(Invoice.TOTAL, BigDecimal.ZERO);
    }
}

4.7. Testing Best Practices

  1. Use DomainTest for all entity types - Even simple entities benefit from the comprehensive validation

  2. Test with transactions - Always rollback to keep tests isolated

  3. Test observable behavior - Verify that model state changes trigger appropriate notifications

  4. Test validation rules - Ensure domain constraints are properly enforced

  5. Test computed values - Verify derived attributes and denormalized values update correctly

  6. Keep tests focused - Each test should verify one specific behavior

  7. Use realistic test data - Your entity factories should create valid, meaningful entities

5. Server

The Codion server provides RMI and HTTP connection options to clients.

5.1. Features

  • Firewall friendly RMI; uses one way communication without callbacks, uses two ports, one for the RMI Registry and one for client connections

  • Integrated web server for serving HTTP client connections, based on Javalin and Jetty

  • All user authentication left to the database by default

  • Comprehensive administration and monitoring facilities via the ServerMonitor

  • Featherweight server with moderate memory and CPU usage

5.2. Security

Here’s a great overview of RMI security risks and mitigations.

5.2.1. Authentication

The Codion server does not perform any user authentication by default, it leaves that up the underlying database. An authentication layer can be added by implementing an Authenticator and registering it with the ServiceLoader.

Authenticator examples

5.2.2. RMI SSL encryption

To enable SSL encryption between client and server, create a keystore and truststore pair and set the following system properties.

Server side
codion.server.connection.sslEnabled=true # (1)
javax.net.ssl.keyStore=keystore.jks
javax.net.ssl.keyStorePassword=password
  1. This property is 'true' by default, included here for completeness’s sake

Client side
codion.client.trustStore=truststore.jks
codion.client.trustStorePassword=password

5.2.3. Class loading

No dynamic class loading is required.

5.2.4. Serialization filtering

The framework provides a way to configure a ObjectInputFilter for deserialization, by specifying a ObjectInputFilterFactory implementation class with the following system property.

codion.server.objectInputFilterFactory=\
    my.serialization.filter.MyObjectInputFilterFactory
Important
By default, an ObjectInputFilterFactory is required for the server to start. If no filter factory is configured, the server will throw an exception on startup. This is a security measure to prevent accidental deployment without deserialization filtering.

To explicitly disable this requirement (not recommended for production), set:

codion.server.objectInputFilterFactoryRequired=false
Pattern filter

To use the built-in pattern based serialization filter, set the following system property.

codion.server.objectInputFilterFactory=\
    is.codion.common.rmi.server.SerializationFilterFactory

To use serialization filter patterns specified in a string, set the following system property.

Important
SerializationFilterFactory automatically appends the exclude all pattern !* if it’s missing, but it is good practice to always include it in your pattern list.
codion.server.serialization.filter.pattern=pattern1;pattern2;!*

This is equivalent to setting the following:

jdk.serialFilter=pattern1;pattern2;!*

To use the serialization pattern filter based on patterns in a file, set the following system property.

The file may contain all the patterns in a single line, using the ; delimiter or one pattern per line, without a delimiter. Lines starting with '#' are skipped as comments.

codion.server.serialization.filter.patternFile=config/patterns.txt
codion.server.serialization.filter.patternFile=classpath:patterns.txt

A list of deserialized classes can be created during a server dry-run by adding the following system property. The file containing all classes deserialized during the run is written to disk on server shutdown.

codion.server.serialization.filter.dryRunFile=deserialized.txt
Example whitelist
java.lang.**
java.math.**
java.time.**
java.util.**
is.codion.common.**
is.codion.framework.**
is.codion.plugin.jasperreports.*
is.codion.demos.chinook.domain.api.*
is.codion.demos.world.domain.api.*
# ServerMonitor
ch.qos.logback.classic.Level
# For loading JasperReport from classpath
net.sf.jasperreports.compilers.**
net.sf.jasperreports.engine.**
# Reject all other classes
!*
Resource exhaustion limits

The pattern-based serialization filter supports JEP 290 resource limits to prevent resource exhaustion attacks during deserialization. These limits are enforced during deserialization, before objects are fully materialized in memory.

The following system properties configure the resource limits (with their default values):

codion.server.serialization.filter.maxBytes=10485760    # Maximum stream size: 10 MB
codion.server.serialization.filter.maxArray=100000      # Maximum array length: 100,000 elements
codion.server.serialization.filter.maxDepth=100         # Maximum object graph depth: 100 levels
codion.server.serialization.filter.maxRefs=1000000      # Maximum internal references: 1,000,000 refs

These limits are automatically prepended to the serialization filter patterns. For example, with a pattern file containing class names, the effective filter becomes:

maxbytes=10485760;maxarray=100000;maxdepth=100;maxrefs=1000000;java.lang.String;is.codion.**;!*

Important: These limits provide the primary defense against resource exhaustion attacks. Application-level validation (such as User credential length limits) cannot prevent memory allocation during deserialization, making these JEP 290 limits essential for security.

Serialization Filter Dry Run

The serialization filter dry-run mode helps you discover exactly which classes are deserialized during program execution. This is essential when creating or updating serialization filter patterns, as it eliminates guesswork about what classes need to be whitelisted.

How It Works

When dry-run mode is enabled, the framework records every class that gets deserialized during the program’s lifetime. On JVM shutdown, it writes a sorted list of unique class names to the specified file. This file can then be used directly as input for the pattern-based filter.

Important
During dry-run mode, all classes are accepted (no filtering is performed). This allows you to capture the complete set of deserialized classes without being blocked by an incomplete filter.
Configuration

Enable dry-run mode by setting the following system property:

codion.server.serialization.filter.dryRunFile=deserialized_classes.txt

The specified file will be created (or overwritten) on JVM shutdown with the discovered class names.

Periodic Flushing

To prevent data loss if the JVM crashes during dry-run, the results are automatically flushed to disk periodically (default: every 30 seconds):

codion.server.serialization.filter.dryRunFlushInterval=30

Set dryRunFlushInterval=0 to disable periodic flushing. For dry-runs in unstable environments, consider setting a shorter interval (e.g., dryRunFlushInterval=5).

Direct Usage Example

The dry-run mode can be used outside of the server context to analyze deserialization in any Java application. Here’s an example that discovers which classes are deserialized when loading a JasperReports report:

public static void main(String[] args) {
    // Set the dry-run output file
    SerializationFilterFactory.SERIALIZATION_FILTER_DRYRUN_FILE.set(
        "/path/to/output.txt");

    // Configure the filter in dry-run mode
    ObjectInputFilter.Config.setSerialFilter(
        new SerializationFilterFactory().createObjectInputFilter());

    // Perform operations that deserializes data
    Report.REPORT_PATH.set("path/to/reports");
    JRReport report = JasperReports.fileReport("customer_report.jasper");
    report.load();

    // Dry-run output is written on JVM shutdown
}
Workflow
  1. Enable dry-run mode with the dryRunFile property

  2. Run your application through typical usage scenarios to trigger deserialization

  3. Shut down the JVM gracefully to write the discovered classes to the file

  4. Review the output file containing the sorted list of class names

  5. Use the output as your whitelist by setting it as the pattern file:

codion.server.serialization.filter.patternFile=deserialized_classes.txt
Array Handling

The dry-run mode automatically handles array types by extracting and recording the component type. For example, if java.lang.String[] is deserialized, the output file will contain java.lang.String, not the array type itself.

Best Practices
  • Run dry-run mode in a test environment that exercises all application features

  • Include all expected user workflows to ensure complete coverage

  • Use dry-run mode when adding new features that might deserialize new types

  • Keep the dry-run output file under version control to track changes over time

  • Re-run dry-run mode after dependency upgrades that might introduce new deserialized classes

5.3. HTTP

Codion provides HTTP-based connections as an alternative to RMI, accessible via the HttpEntityConnection interface. The framework supports two distinct HTTP connection types, each with different serialization strategies optimized for different scenarios.

5.3.1. Connection Types

Serialization-based Connection

The default HTTP connection uses Java serialization for all communication:

  • Performance: More efficient for complex object graphs with automatic deduplication

  • Memory: Lower memory footprint due to object reference handling

  • Security: Requires proper deserialization filtering (see Serialization filtering)

  • Configuration: Server-side enabled via codion.server.http.serialization=true (default: false)

The serialization-based connection is ideal for trusted networks and scenarios where performance is critical.

JSON-based Connection

The JSON connection provides a more security-conscious approach:

  • Security: Eliminates server-side deserialization vulnerabilities by using JSON for incoming requests

  • Transparency: Human-readable request/response format for debugging

  • Compatibility: Works with non-Java clients (when using JSON throughout)

  • Configuration: Server-side enabled via codion.server.http.json=true (default: true)

The JSON connection uses a hybrid serialization strategy:

Operation Request Response

CRUD

JSON

JSON

Functions

JSON

Java serialization

Procedures

JSON

N/A

Reports

JSON

Java serialization

Entities

N/A

Java serialization

Important
The hybrid approach protects the server from deserialization attacks while maintaining compatibility with complex return types like JasperPrint and Entities that don’t support JSON serialization.

5.3.2. Choosing a Connection Type

Use serialization-based connections when:

  • Operating in a trusted network environment

  • Performance and memory efficiency are priorities

  • You have proper deserialization filtering configured

  • All clients are Java-based

Use JSON-based connections when:

  • Security is the primary concern (avoiding server-side deserialization)

  • Operating in less trusted environments

  • You want human-readable request/response payloads

  • You need non-Java client compatibility (with custom JSON handling)

5.3.3. JSON Connection Requirements

When using the JSON connection with functions, procedures, or reports that have parameters, you must register parameter types with the EntityObjectMapper. This is required because Jackson needs target types to deserialize JSON, and generic type parameters are erased at runtime.

See the manual section on HTTP/JSON Serialization for details on registering parameter types via EntityObjectMapperFactory.

5.3.4. Server Configuration

Enable or disable connection types on the server:

codion.server.http.serialization=false  # Default: false
codion.server.http.json=true            # Default: true

Both can be enabled simultaneously, allowing clients to choose their preferred connection type.

5.3.5. Client Configuration

Clients select their connection type via configuration:

codion.client.http.json=true   # Default: true (JSON), false for serialization

5.4. Configuration

5.4.1. Example configuration file

# Database configuration
codion.db.url=jdbc:h2:mem:h2db
codion.db.useOptimisticLocking=true
codion.db.countQueries=true
codion.db.initScripts=\
    config/employees/create_schema.sql,\
    config/chinook/create_schema.sql,\
    config/petstore/create_schema.sql,\
    config/world/create_schema.sql

# The admin user credentials, used by the server monitor application
codion.server.admin.user=scott:tiger

# Client method tracing disabled by default
codion.server.methodTracing=false

# A connection pool based on this user is created on startup
codion.server.connectionPoolUsers=scott:tiger

# The port used by clients
codion.server.port=2222

# The port for the admin interface, used by the server monitor
codion.server.admin.port=4444

# RMI Registry port
codion.server.registryPort=1099

# Any auxiliary servers to run alongside this server
codion.server.auxiliaryServerFactories=\
    is.codion.framework.servlet.EntityServiceFactory

# Specifies whether to expose json based services (default true)
codion.server.http.json=true

# Specifies whether to expose java serialization based services (default false)
codion.server.http.serialization=true

# The http port
codion.server.http.port=8080

# Specifies whether to use https
codion.server.http.secure=false

# The ObjectInputFilterFactory class to use
codion.server.objectInputFilterFactory=\
    is.codion.common.rmi.server.SerializationFilterFactory

# The serialization pattern file to use for RMI deserialization filtering
codion.server.serialization.filter.patternFile=\
    ../config/serialization-whitelist.txt

# RMI configuration
java.rmi.server.hostname=localhost
java.rmi.server.randomIDs=true

# SSL configuration
javax.net.ssl.keyStore=../config/keystore.jks
javax.net.ssl.keyStorePassword=crappypass

# Used to connect to the server to shut it down
#codion.client.trustStore=../config/truststore.jks

5.5. Code examples

Absolute bare-bones examples of how to run the EntityServer and connect to it.

5.5.1. RMI

    Database database = H2DatabaseFactory
            .createDatabase("jdbc:h2:mem:testdb",
                    "src/main/sql/create_schema.sql");

    EntityServerConfiguration configuration =
            EntityServerConfiguration.builder(SERVER_PORT, REGISTRY_PORT)
                    .domainClasses(List.of(Store.class.getName()))
                    .database(database)
                    .sslEnabled(false)
                    .build();

    EntityServer server = EntityServer.startServer(configuration);

    RemoteEntityConnectionProvider connectionProvider =
            RemoteEntityConnectionProvider.builder()
                    .port(SERVER_PORT)
                    .registryPort(REGISTRY_PORT)
                    .domain(Store.DOMAIN)
                    .user(parse("scott:tiger"))
                    .build();

    EntityConnection connection = connectionProvider.connection();

    List<Entity> customers = connection.select(all(Customer.TYPE));
    customers.forEach(System.out::println);

    connection.close();

    server.shutdown();

5.5.2. HTTP

    Database database = H2DatabaseFactory
            .createDatabase("jdbc:h2:mem:testdb",
                    "src/main/sql/create_schema.sql");

    EntityService.PORT.set(HTTP_PORT);

    EntityServerConfiguration configuration =
            EntityServerConfiguration.builder(SERVER_PORT, REGISTRY_PORT)
                    .domainClasses(List.of(Store.class.getName()))
                    .database(database)
                    .sslEnabled(false)
                    .auxiliaryServerFactory(List.of(EntityServiceFactory.class.getName()))
                    .build();

    EntityServer server = EntityServer.startServer(configuration);

    HttpEntityConnectionProvider connectionProvider =
            HttpEntityConnectionProvider.builder()
                    .port(HTTP_PORT)
                    .https(false)
                    .domain(Store.DOMAIN)
                    .user(parse("scott:tiger"))
                    .build();

    EntityConnection connection = connectionProvider.connection();

    List<Entity> customers = connection.select(all(Customer.TYPE));
    customers.forEach(System.out::println);

    connection.close();

    server.shutdown();

6. Server Monitor

The Codion Server Monitor provides a way to monitor the Codion server.

Below are screenshots of the different server monitor tabs, after ~1 1/2 hours of running the Chinook load test, with ~10 minutes of ramping up to 100 client instances. The server is running on a Raspberry Pi 4, Ubuntu Server 20.10, JDK 19, -Xmx256m, using a HikariCP connection pool on top of an H2 in-memory database.

6.1. Server performance

Server performance

6.2. Connection pools

Connection pool

6.3. Database performance

Database performance

6.4. Clients & users

Clients users

6.5. Environment

6.5.1. System

System

6.5.2. Entities

Domain

6.5.3. Operations

Domain

7. Code style and design

7.1. Factories and builders

Most concrete framework classes, which implement a public interface, are final, package private, and are instantiated with the help of static methods in the interface they implement.

7.1.1. Factories

Static factory methods are provided for classes with a simple state. These are usually named after the interface, which makes using static imports quite convenient.

Event<String> event = event(); // Event.event()

Value<Integer> value = Value.nullable();

State state = State.state(true);

EntityTableConditionModel conditionModel =
        entityTableConditionModel(Customer.TYPE, connectionProvider);

7.1.2. Builders

For classes with a more complex state, a builder method is provided in the interface.

TaskScheduler scheduler =
        TaskScheduler.builder()
                .task(() -> {})
                .interval(5, TimeUnit.SECONDS)
                .initialDelay(15)
                .build();

TemporalField<LocalDate> field =
        TemporalField.builder()
                .temporalClass(LocalDate.class)
                .dateTimePattern("dd.MM.yyyy")
                .columns(12)
                .border(createTitledBorder("Date"))
                .build();

7.2. Accessors

Immutable fields are accessed using methods named after the field, without a get/is prefix.

Observer<String> observer = event.observer();

LocalEntityConnection connection = connectionProvider.connection();

boolean modified = entity.modified();

Entity.Key primaryKey = entity.primaryKey();

A get/is prefix implies that the field is mutable and that a corresponding setter method exists, with a set prefix.

7.2.1. Observables

Many classes expose their internal state via the Value class, which can be used to mutate the associated state or observe it by adding listeners or consumers.

FilterListSelection<List<String>> selection = tableModel.selection();

List<Integer> selectedIndexes = selection.indexes().get();

selection.indexes().set(List.of(0, 1, 2));

selection.items().addListener(() -> System.out.println("Selected items changed"));

table.sortable().set(false);

7.3. Exceptions

There are of course some exceptions to these rules, such as a get prefix on an accessor for a functionally immutable field or a is prefix on an immutable boolean field, but these exceptions are usually to keep the style of a class being extended, such as Swing components and should be few and far between.

Value<Integer> integer = Value.nullable();

boolean isNull = integer.isNull();
boolean isNullable = integer.isNullable();

ValueList<Integer> integers = ValueList.valueList();

boolean isEmpty = integers.isEmpty();

8. Modules

8.1. Common

Common classes used throughout the framework.

codion-common-bom

codion-common-core

Dependency graph
dependency graph

codion-common-db

JDBC related classes.

Dependency graph
dependency graph

codion-common-model

Common model classes.

Dependency graph
dependency graph

codion-common-i18n

Dependency graph
dependency graph

codion-common-rmi

RMI related classes.

Dependency graph
dependency graph

8.2. DBMS

Database specific implementation classes.

codion-dbms-db2

Dependency graph
dependency graph

codion-dbms-derby

Dependency graph
dependency graph

codion-dbms-h2

Dependency graph
dependency graph

codion-dbms-hsqldb

Dependency graph
dependency graph

codion-dbms-mariadb

Dependency graph
dependency graph

codion-dbms-mysql

Dependency graph
dependency graph

codion-dbms-oracle

Dependency graph
dependency graph

codion-dbms-postgresql

Dependency graph
dependency graph

codion-dbms-sqlite

Dependency graph
dependency graph

codion-dbms-sqlserver

Dependency graph
dependency graph

8.3. Framework

The framework itself.

codion-framework-bom

codion-framework-domain

Domain model related classes.

Dependency graph
dependency graph

codion-framework-domain-db

Domain model generation from a database schema.

Dependency graph
dependency graph

codion-framework-domain-test

Domain model unit test related classes.

Dependency graph
dependency graph

codion-framework-db-core

Core database connection related classes.

Dependency graph
dependency graph

codion-framework-db-local

Local JDBC connection related classes.

Dependency graph
dependency graph

codion-framework-db-rmi

RMI connection related classes.

Dependency graph
dependency graph

codion-framework-db-http

HTTP connection related classes.

Dependency graph
dependency graph

codion-framework-i18n

Internationalization strings.

Dependency graph
dependency graph

codion-framework-json-domain

Dependency graph
dependency graph

codion-framework-json-db

Dependency graph
dependency graph

codion-framework-model

Common framework model classes.

Dependency graph
dependency graph

codion-framework-model-test

General application model unit test related classes.

Dependency graph
dependency graph

codion-framework-server

Framework server classes.

Dependency graph
dependency graph

codion-framework-servlet

HTTP servlet server classes.

Dependency graph
dependency graph

8.5. Tools

codion-tools-server-monitor-model

Dependency graph
dependency graph

codion-tools-server-monitor-ui

Dependency graph
dependency graph

8.5.1. Generator

codion-tools-generator-domain

Dependency graph
dependency graph

codion-tools-generator-model

Dependency graph
dependency graph

codion-tools-generator-ui

Dependency graph
dependency graph

8.5.2. Load Test

codion-tools-loadtest-core

Dependency graph
dependency graph

codion-tools-loadtest-model

Dependency graph
dependency graph

codion-tools-loadtest-ui

Dependency graph
dependency graph

8.6. Plugins

8.6.1. Logging

codion-plugin-jul-proxy
Dependency graph
dependency graph
codion-plugin-log4j-proxy
Dependency graph
dependency graph
codion-plugin-logback-proxy
Dependency graph
dependency graph

8.6.2. Connection pools

codion-plugin-hikari-pool
Dependency graph
dependency graph
codion-plugin-tomcat-pool
Dependency graph
dependency graph

8.6.3. Reporting

codion-plugin-jasperreports
Dependency graph
dependency graph

8.6.4. Look & Feel

Provides all available Flat Look & Feels.

codion-plugin-flatlaf
Dependency graph
dependency graph
codion-plugin-flatlaf-intellij-themes

Provides a bunch of IntelliJ Theme based Flat Look & Feels.

Dependency graph
dependency graph

8.6.5. Other

codion-plugin-imagepanel
Dependency graph
dependency graph
codion-plugin-swing-robot
Dependency graph
dependency graph
codion-plugin-swing-mcp
Dependency graph
dependency graph

9. Utilities

9.1. IntelliJ IDEA

9.1.1. Live templates

Here are a few live templates for IntelliJ, reducing the typing required when defining a domain model.

Add this file to the templates directory in the IntelliJ IDEA configuration directory.

View template file
<templateSet group="codion">
  <template name="cod" value="Column&lt;Double&gt; $ATTRIBUTE_NAME$ = TYPE.doubleColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;Double&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="coi" value="Column&lt;Integer&gt; $ATTRIBUTE_NAME$ = TYPE.integerColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;Integer&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="col" value="Column&lt;Long&gt; $ATTRIBUTE_NAME$ = TYPE.longColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;Long&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="cos" value="Column&lt;String&gt; $ATTRIBUTE_NAME$ = TYPE.stringColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;String&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="fk" value="ForeignKey $ATTRIBUTE_NAME$ = TYPE.foreignKey(&quot;$FK_NAME$&quot;, $END$);" description="ForeignKey" toReformat="false" toShortenFQNames="true">
    <variable name="FK_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, FK_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="cold" value="Column&lt;LocalDate&gt; $ATTRIBUTE_NAME$ = TYPE.localDateColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;LocalDate&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="coldt" value="Column&lt;LocalDateTime&gt; $ATTRIBUTE_NAME$ = TYPE.localDateTimeColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;LocalDateTime&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="et" value="EntityType TYPE = DOMAIN.entityType(&quot;$TABLE_NAME$&quot;);$END$" description="EntityType" toReformat="false" toShortenFQNames="true">
    <variable name="TABLE_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="cob" value="Column&lt;Boolean&gt; $ATTRIBUTE_NAME$ = TYPE.booleanColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;Boolean&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="cosh" value="Column&lt;Short&gt; $ATTRIBUTE_NAME$ = TYPE.shortColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;Short&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="coc" value="Column&lt;Character&gt; $ATTRIBUTE_NAME$ = TYPE.characterColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;Character&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="coodt" value="Column&lt;OffsetDateTime&gt; $ATTRIBUTE_NAME$ = TYPE.offsetDateTimeColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;OffsetDateTime&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
  <template name="coby" value="Column&lt;byte[]&gt; $ATTRIBUTE_NAME$ = TYPE.byteArrayColumn(&quot;$COLUMN_NAME$&quot;);$END$" description="Column&lt;byte[]&gt;" toReformat="false" toShortenFQNames="true">
    <variable name="COLUMN_NAME" expression="" defaultValue="" alwaysStopAt="true" />
    <variable name="ATTRIBUTE_NAME" expression="groovyScript(&quot;_1.toUpperCase()&quot;, COLUMN_NAME)" defaultValue="" alwaysStopAt="true" />
    <context>
      <option name="JAVA_DECLARATION" value="true" />
    </context>
  </template>
</templateSet>
Available templates
Name Template

et

EntityType TYPE = DOMAIN.entityType("table_name");

fk

ForeignKey FK_KEY = TYPE.foreignKey("fk_key");

cosh

Column<Short> COLUMN = TYPE.shortColumn("column");

coi

Column<Integer> COLUMN = TYPE.integerColumn("column");

col

Column<Long> COLUMN = TYPE.longColumn("column");

cod

Column<Double> COLUMN = TYPE.doubleColumn("column");

cos

Column<String> COLUMN = TYPE.stringColumn("column");

cold

Column<LocalDate> COLUMN = TYPE.localDateColumn("column");

coldt

Column<LocalDateTime> COLUMN = TYPE.localDateTimeColumn("column");

coodt

Column<OffsetDateTime> COLUMN = TYPE.offsetDateTimeColumn("column");

cob

Column<Boolean> COLUMN = TYPE.booleanColumn("column");

coc

Column<Character> COLUMN = TYPE.characterColumn("column");

coby

Column<byte[]> COLUMN = TYPE.byteArrayColumn("column");

10. Internationalization (i18n)

10.1. Overriding

The default i18n strings can be overridden by implementing Resources and registering the implementation with the ServiceLoader.

package is.codion.demos.chinook.i18n;

import is.codion.common.resource.Resources;
import is.codion.framework.i18n.FrameworkMessages;

import java.util.Locale;

/**
 * Replace the english modified warning text and title.
 */
public final class ChinookResources implements Resources {

  private static final String FRAMEWORK_MESSAGES =
          FrameworkMessages.class.getName();

  private final boolean english = Locale.getDefault()
          .equals(new Locale("en", "EN"));

  @Override
  public String getString(String baseBundleName, String key, String defaultString) {
    if (english && baseBundleName.equals(FRAMEWORK_MESSAGES)) {
      return switch (key) {
        case "modified_warning" -> "Unsaved changes will be lost, continue?";
        case "modified_warning_title" -> "Unsaved changes";
        default -> defaultString;
      };
    }

    return defaultString;
  }
}
module is.codion.demos.chinook {

  ...

  provides is.codion.common.resource.Resources
            with is.codion.demos.chinook.domain.impl.ChinookResources;
}

10.2. i18n Property Values

For a complete reference of all available i18n property keys and their default values, see i18n Property Values Reference.

11. Architecture Deep Dives

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

11.1. Architecture Deep Dive: Observable Pattern

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

11.1.2. Core Architecture

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.

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

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 notification access only
}

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

11.1.3. Value Implementation

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

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

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

11.1.6. Thread Safety

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

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

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

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

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

11.1.7. Memory Management

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

11.1.8. Performance Characteristics

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

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

11.1.10. Integration Examples

UI Component Binding
// Swing component binding
ComponentValue<JTextField, String> textFieldValue = Components.stringField().build();
Value<String> modelValue = Value.value("");

// Bidirectional binding
textFieldValue.link(modelValue);
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;
    }
}
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.1.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.