Domain Entities UI

2024 11 01 head

Commercial digital applications regularly display a list of entities and details for the selected one. The selected entity can be displayed, edited, and deleted. A new entity can be created. This pattern is called CRUD.

CRUD solutions permeate the digital world. Most company internal applications are a variation of this pattern.

Often you do not need sophisticated forms or display complex diagrams. But most of the solutions will have a few complex forms or diagrams. It is also common to have a few workflows codifying key business processes of your company.

The Naked Objects approach is a radical simplification of the CRUD pattern. We use it for all our management applications.

Domain-driven Design

Domain-driven design practitioners complain that it was hard to gain commitment from business stakeholders, or even to completely engage them. My own experience suggests that it was nearly impossible to engage business managers with UML or Business Process Model and Notation diagrams. It was much easier to engage them in rapid prototyping where they could see and interact with the results. Another approach is holding event storming sessions, where the business managers can see the domain model emerge in real-time.

Even if you could engage the sponsors to design a domain model, by the time you have finished developing the system on top, most of its benefits had disappeared. It is nice to create an agile domain object model. But if any change to that model also dictates the modification of the layers above and underneath, then that agility is worthless.

The other concern that gave rise to the birth of Naked Objects was how to make user interfaces of mainstream business systems expressive. Most business systems are not at all expressive. They treat the user merely as a dumb process-follower, rather than as an empowered problem-solver.

Domain-driven design realized that if the domain model represents the ubiquitous language of the business, those domain objects should be behaviorally rich. Regular business logic is encapsulated as methods on the domain objects rather than in procedural scripts on top of them. Complex rules are expressed as business logic classes collaborating with domain entities.

The next step is naturally to have the user interface be a reflection of the domain model. It would make it easier to apply domain-driven design because one could easily translate evolving domain modeling ideas into a working prototype. And it would deliver an expressive, object-oriented user interface for free.

Simple things should be simple, complex things should be possible. We avoid annotation-based solutions because we believe plain code is more legible and maintainable. Our preferred technology stack is pure modern Java and {ref-vaadin} for the user interface.

Grid Display

A grid display of an entity list is the bread and butter of the domain entities representation.

The additional concepts are filtering and ordering of the selected items per column [1].

The standard read, update, delete and create operations are supported.

An additional creation variant is the duplicate operation to minimize user inputs. Good user interface design is avoiding the user to enter the same data multiple times.

The source code to create a grid with custom columns is:

class EffortsView extends ItemView<Effort> {
    // constant declarations

    public EffortsView(@NotNull ProductsBoundedDomainUi domain, @NotNull Mode mode) {
        super(Effort.class, domain, domain.efforts(), new EffortFilter(), mode);
        form(() -> new EffortForm(this));
        init();
    }

    private void init() {
        var grid = grid();
        grid.addColumn(o ->
            Objects.nonNull(o.assignment()) ? o.assignment().id() : null)
                .setKey(ASSIGNMENT).setHeader(ASSIGNMENT_LABEL).setAutoWidth(true)
                .setResizable(true).setSortable(true);
        grid.addColumn(o -> Objects.nonNull(o.assignment()) ? o.assignment().name() : null)
                .setKey(COLLABORATOR).setHeader(COLLABORATOR_LABEL)
            .setAutoWidth(true).setResizable(true).setSortable(true);
        grid.addColumn(Effort::date).setKey(DATE).setHeader(DATE_LABEL).setAutoWidth(true)
            .setResizable(true).setSortable(true);
        grid.addColumn(Effort::duration).setKey(DURATION).setHeader(DURATION_LABEL)
            .setAutoWidth(true).setResizable(true).setSortable(true);
        grid.addColumn(Effort::contractId).setKey(CONTRACT_ID).setHeader(CONTRACT_ID_LABEL)
            .setAutoWidth(true).setResizable(true).setSortable(true);
    }
}

The source code to create a custom form for the CRUD operations is:

class EffortForm extends ItemForm<Effort, EffortsView> {

    EffortForm(@NotNull EffortsView parent) {
        super(parent);
        addTabAt("details", details(), 0);
        addTabAt("text", textForm(), 1);
    }

    protected FormLayout details() {
        TextField assignment = VaadinUtils.createTextField(ASSIGNMENT_LABEL, ASSIGNMENT,
            true, false);
        TextField collaborator = VaadinUtils.createTextField(COLLABORATOR_LABEL, COLLABORATOR,
            true, false);
        TextField collaboratorId = VaadinUtils.createTextField("Collaborator ID",
            "collaborator id", true, false);
        TextField contractId = new TextField(CONTRACT_ID_LABEL, CONTRACT_ID);
        DatePicker date = VaadinUtils.createDatePicker(DATE_LABEL);
        IntegerField duration = new IntegerField(DURATION_LABEL, DURATION);

        FormLayout form = new FormLayout();
        form.add(assignment, collaborator, collaboratorId, contractId, date, duration);

        binder().bindReadOnly(assignment, o -> o.assignment().id());
        binder().bindReadOnly(collaborator, o -> o.assignment().name());
        binder().bindReadOnly(collaboratorId, o -> o.assignment().collaboratorId());
        binder().bind(contractId, Effort::contractId, Effort::contractId);
        binder().bind(date, Effort::date, Effort::date);
        binder().bind(duration, Effort::duration, Effort::duration);
        return form;
        }

        @Override
        protected Effort createOrUpdateInstance(Effort entity) throws ValidationException {
            return Objects.isNull(entity) ? new Effort() : entity;
        }
    }

The code for implementing the same functionally for a subclass of `net.tangly.core.Entity' is more compact due to convention over configuration.

Please consult the documentation of the component net.tangly.ui and the associated JavaDoc.

Business Operations

Advanced Cell display

Vaadin supports sophisticated grid cell extension. An example to format accounting numbers and to color them red if negative:

    public static <T> ComponentRenderer<Span, T> coloredRender(Function<T, BigDecimal> getter,
                                    NumberFormat numberFormat) {
        return new ComponentRenderer<>((T e) -> {
            BigDecimal v = getter.apply(e);
            return switch (BigDecimal.ZERO.compareTo(v)) {
                case -1 -> new Span(numberFormat.format(v));
                case 0 -> new Span();
                case 1 -> {
                    Span s = new Span(numberFormat.format(v));
                    s.getElement().getStyle().set("color", "red");
                    yield s;
                }
                default -> new Span("");
            };
        });
    }

Grid Actions

The net.tangly.ui.component.ItemView supports adding selected item and global commands to the grid popup menu.

    protected void addActions(@NotNull GridContextMenu<Assignment> menu) {
        menu().add(new Hr());
        menu().addItem("Print", e ->
            Cmd.ofItemCmd(e, (Assignment o) ->
                new CmdCreateEffortsReport(o, domain()).execute()));        (1)
        menu().addItem("Import", e ->
            Cmd.ofGlobalCmd(e,
                () -> new CmdFilesUploadEfforts(domain()).execute()));      (2)
    }
1 Adds a selected item command which will process the selected item. Optional parameters can be inputted in a dialog before processing.
2 Adds a global command which will process items displayed in the grid. Optional global parameters can be inputted in a dialog before processing.

Asynchronous Operations

Changes in the system are communicated via events. Domain-driven design promotes asynchronous event communication between the bounded contexts.

The same mechanism is used to communicate changes in the domain entities to the multiple UI instances.

The event bus is a simple in-memory event bus using the Java Flow API and default implementation. No additional libraries are necessary.

The processing of received events is asynchronous and performed in background threads.

Internal Events

Internal events are used to notify the system about changes in the domain entities and update the UI accordingly. All internal events are sent over a domain-specific event bus.

Care is taken to publish UI changes on the UI thread. {ref-vaadin} provides a mechanism to execute code on the UI thread.

UI.getCurrent().access(() -> v.ifPresent(View::refresh));              (1)
1 First, the current UI in which the user component is expected is obtained. Second, The action is posted on the user interface thread with the access method.

External Events

A bounded domain can register and receive external events from other domains. Each domain publishes events to a domain-specific public event bus. All public events created by the domain are sent over its public event bus instance.

Interested parties can subscribe to the public event bus and receive events. The receiving and processing of events is done asynchronously in the background threads.

These external events often update the domain model and trigger internal events. These interval events then update the UI instances.

A user interface instance is created for each tab in the browser. Multiple users can work with the same domain entities in parallel.

When a tab or a browser is closed, the user interface instance is destroyed. It triggers a cleanup of the user interface domain entities. One important housekeeping operation unregisters the UI instance from the internal event bus. This logic is implemented in the `BoundedDomainClass' abstraction.

Graphical Extensions

Well-designed graphics can highlight the most important information for the user.

We use the SO-Charts library to display complex diagrams. The library is a wrapper around the very cool Apache Foundation ECharts library. Exhaustive types of diagrams are provided. See Diagram Examples.

Lessons Learnt

The domain entities UI is a key part of the domain-driven design. Mechanisms to display, edit, and create domain entities are essential.

The open source products Causeway and OpenXava provide similar functionality. Their approach is annotation-based. I strongly prefer a programmatic configuration because it is more legible and expendable.

Adding codes, tags, and comments to domain entities was quite simple.

Views for tags and comments are part of the domain entities UI library. Visual components for code and date range are available. It simplifies the creation of a dialog for entities with tags and comments.

The generic views ItemView<T> and EntityView<T> are the foundation of the domain entities UI library. They tremendously simplify the creation of CRUD grid components.

The multi-user and multi-tab support runs in a multithreaded environment and requires careful programming to scale. All the logic is provided by the domain entities UI framework to diminish complexity for the application developers.

The classes defined in the UI library tremendously simplify the realization of regular CRUD grid components. Documentation is available under Vaadin UI Naked Objects.

We provide all our libraries as maven artifacts published on Maven Central.


1. Strong filtering and ordering capabilities make paging obsolete.