Codion and Complexity
A Comparison Experiment
A note on method before the numbers: this is a single experiment, conducted by the Codion author, implementing the same small application in several stacks. The non-Codion line counts are estimates for idiomatic implementations, not competition-grade golf, and reasonable people will arrive at somewhat different numbers. The Codion implementation is reproduced in full below, so at least one side of the comparison you can judge for yourself.
CRUD Implementation Comparison
The baseline was a simple customer management application with:
- A Customer entity (id, first_name)
- Full CRUD operations
- Search functionality
- A master-detail relationship with Address entity (id, customer_id, street)
Codion Implementation
The complete Codion implementation required just over 140 lines of code:
package is.codion.demos.store;
import is.codion.common.db.database.Database;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.domain.DomainModel;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.Column.Generator;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import is.codion.swing.framework.model.SwingEntityApplicationModel;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityApplication;
import is.codion.swing.framework.ui.EntityApplicationPanel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.EntityPanel;
import java.util.List;
import static is.codion.framework.domain.DomainType.domainType;
public final class StoreAppPanel extends EntityApplicationPanel<SwingEntityApplicationModel> {
public static final DomainType DOMAIN = domainType("Store");
public interface Customer {
EntityType TYPE = DOMAIN.entityType("store.customer");
Column<Long> ID = TYPE.longColumn("id");
Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
}
public interface Address {
EntityType TYPE = DOMAIN.entityType("store.address");
Column<Long> ID = TYPE.longColumn("id");
Column<String> STREET = TYPE.stringColumn("street");
Column<Long> CUSTOMER_ID = TYPE.longColumn("customer_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
private static class Store extends DomainModel {
private Store() {
super(DOMAIN);
add(Customer.TYPE.as()
.attributes(
Customer.ID.as()
.primaryKey()
.generator(Generator.identity()),
Customer.FIRST_NAME.as()
.column()
.caption("First name")
.nullable(false)
.maximumLength(40))
.caption("Customer")
.build());
add(Address.TYPE.as()
.attributes(
Address.ID.as()
.primaryKey()
.generator(Generator.identity()),
Address.STREET.as()
.column()
.nullable(false)
.maximumLength(120)
.caption("Street"),
Address.CUSTOMER_ID.as()
.column(),
Address.CUSTOMER_FK.as()
.foreignKey()
.caption("Customer"))
.caption("Address")
.build());
}
}
public StoreAppPanel(SwingEntityApplicationModel applicationModel) {
super(applicationModel, createEntityPanels(applicationModel), List.of());
}
private static List<EntityPanel> createEntityPanels(SwingEntityApplicationModel applicationModel) {
SwingEntityModel customerModel = applicationModel.models().get(Customer.TYPE);
SwingEntityModel addressModel = customerModel.detail().get(Address.TYPE);
EntityPanel customerPanel = new EntityPanel(customerModel, new CustomerEditPanel(customerModel.editModel()));
EntityPanel addressPanel = new EntityPanel(addressModel, new AddressEditPanel(addressModel.editModel()));
customerPanel.detail().add(addressPanel);
return List.of(customerPanel);
}
private static SwingEntityApplicationModel createApplicationModel(EntityConnectionProvider connectionProvider) {
SwingEntityModel customerModel = new SwingEntityModel(Customer.TYPE, connectionProvider);
SwingEntityModel addressModel = new SwingEntityModel(Address.TYPE, connectionProvider);
customerModel.detail().add(addressModel);
return new SwingEntityApplicationModel(connectionProvider, List.of(customerModel));
}
private static class CustomerEditPanel extends EntityEditPanel {
public CustomerEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
create().textField(Customer.FIRST_NAME);
addInputPanel(Customer.FIRST_NAME);
}
}
private static class AddressEditPanel extends EntityEditPanel {
public AddressEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
create().comboBox(Address.CUSTOMER_FK);
create().textField(Address.STREET);
addInputPanel(Address.CUSTOMER_FK);
addInputPanel(Address.STREET);
}
}
public static void main(String[] args) {
Database.URL.set("jdbc:h2:mem:h2db");
Database.INIT_SCRIPTS.set("src/main/sql/create_schema.sql");
EntityApplication.builder(SwingEntityApplicationModel.class, StoreAppPanel.class)
.connectionProvider(LocalEntityConnectionProvider.builder()
.domain(new Store())
.user(User.parse("scott:tiger"))
.build())
.model(StoreAppPanel::createApplicationModel)
.start();
}
}
This implementation includes:
- Full CRUD operations for both entities
- Master-detail filtering (automatic)
- Search functionality
- Form validation
- Keyboard navigation
- Valid/modified indicators
- Multi-item editing
- Column filtering and sorting
- Denormalized export capabilities
- Responsive table components
Framework Comparison Results
| Framework | Lines of Code | Cost of adding master-detail |
|---|---|---|
| Codion | 143 | +51 lines (+55%) |
| Spring Boot + Thymeleaf | ~355 | +170 lines (+92%) |
| JavaFX + Spring Boot | ~500 | +210 lines (+72%) |
| Vaadin + Spring Boot | ~335 | +155 lines (+86%) |
| React + Node.js | ~420 | +180 lines (+75%) |
| Angular + .NET Core | ~480 | +200 lines (+71%) |
Key Observations
The Cost of a Detail Table
Adding a simple master-detail relationship revealed clear differences:
- Codion: Added just 51 lines with automatic filtering and synchronization
- Other frameworks: Required 155-210 additional lines with manual coordination
- Average complexity increase: Codion 55% vs. others 81%
Manual vs. Automatic Patterns
Traditional frameworks required manual implementation of:
- Master-detail selection coordination
- Filtering detail records by master
- Refresh patterns after updates
- State management between entities
- Visibility toggling for UI sections
- Error handling duplication
Codion handles these patterns automatically through its framework design.
Feature Parity Gap
Despite having 2-3x more code, other frameworks still lacked many of Codion’s built-in features:
- Comprehensive keyboard navigation
- Valid/modified attribute indicators
- Multi-item editing capabilities
- Advanced per-column filtering
- Export functionality
- Automatic UI generation from domain model
Feature Parity Requirements
Here’s what’s needed to achieve feature parity with Codion using other frameworks:
Basic Business Table Requirements
| Feature | Lines of Code | Framework Implementation |
|---|---|---|
| Sortable columns | ~50 | Custom sort logic, state management, UI indicators |
| Resizable columns | ~100 | Mouse event handlers, column width calculations, persistence |
| Column reordering | ~150 | Drag & drop implementation, array manipulation, state sync |
| Per-column filtering | ~200 | Filter UI components, query building, debounced search |
| Keyboard navigation | ~300 | Key event handlers, focus management, selection logic |
| Export functionality | ~150 | Data serialization, clipboard API, format options |
| Multi-row selection | ~100 | Selection state, Ctrl/Shift logic, visual indicators |
| Modified field indicators | ~100 | Dirty checking, visual states, form coordination |
| Master-detail coordination | ~500 | Manual filtering, refresh orchestration, state sync |
| Professional validation | ~200 | Field-level validation, error display, form states |
Total additional lines needed: ~1,850
This transforms our comparison from:
- Codion: 143 lines
- Other frameworks: ~355-510 lines
To the reality of feature parity:
- Codion: 143 lines (full enterprise functionality)
- Other frameworks: 355-510 + 1,850 = 2,205-2,360 lines
Development Time Impact
Rough estimates of what the code difference means in practice:
Development time:
- Basic CRUD: days, with any of these frameworks
- The table and form features above: weeks of additional work
- Testing the interaction combinations, browser compatibility where applicable: more weeks
Maintenance:
- Framework upgrades can break hand-rolled table logic
- Feature additions touch multiple interconnected systems
- Bug fixes ripple through custom component hierarchies
What usually happens instead: Applications ship without the features:
- Column resizing (“users can work around it”)
- Keyboard navigation (“they can use the mouse”)
- Export functionality (“we’ll add it later”)
- Proper validation feedback (“basic validation is enough”)
Conclusion
The pattern this experiment suggests: many frameworks leave developers to hand-implement functionality that could be framework-provided. For this class of application the difference was roughly an order of magnitude in code once feature parity was accounted for - along with the development time and maintenance surface that code implies, or the quiet feature cuts made to avoid writing it.
Codion takes a different approach - optimizing specifically for business application requirements. Its development has focused on business application needs. Explore the complete Codion philosophy →
These findings suggest that some internal business applications could benefit from desktop implementations using domain-focused frameworks. This presents an alternative to web deployment for certain enterprise tools. Read why desktop applications still matter →