Clean Architecture Application with Vaadin Flow

George
Technical blog from UNIL engineering teams
12 min readSep 10, 2024
Photo by Timothe Courtier on Unsplash

Let’s look at how we may use Vaadin Flow for development of Clean DDD applications. We shall take a look at a “Hello World!” example application but one which implements several components from each layer of Clean Architecture using most of the techniques we have seen previously. These include:

This last point is the main focus of this article. For the other points, listed above, we strongly encourage the reader to consult the related resources (from this blog) for the necessary context.

Domain

The source code for our application is available from GitHub. It’s a variation of an example used in the Flow documentation. A user is presented with a form where she can type a name and choose one of the available languages. The system then will output a greeting in the selected language.

An UI form in Vaadin Flow with a textfield, a combobox, and a button. A greeting message is displayed below the form.
Simple Vaadin Flow application

When we are greeting someone we must be aware of several factors. These will most certainly include the name of the person we are addressing and the linguistic (or cultural) environment comprehensible to the addressee. These factors can actually be quite complex and are the subject of study by linguists and anthropologists. For example, social relationship between the annunciator and the addressee and/or age of the addressee may come in to the consideration when choosing an appropriate greeting formula. In French, for example, one would choose between “Bonjour !” or “Salut !” depending on whether one would address a person using polite version of the second person pronoun (“vous” vs “tu”).

We start by capturing the above-mentionned characteristics in our model. For the sake of simplicity we focus on two factors which determine what we shall mean by a Greeting in our problem space: name of the addressee and the locale for the greeting (message). These become the mandatory attributes of our principle Value Object.

@Value
public class Greeting {

String addressee;
Locale locale;

/**
* Constructs a greeting for a given {@code addressee} in the provided
* {@code locale}.
*/
public Greeting(String addressee, String languageTag) {
this.addressee = validAddressee(addressee);
this.locale = validLocale(languageTag);
}

private String validAddressee(String addressee) {
return Optional.ofNullable(addressee)
.filter(s -> !s.isEmpty())
.filter(s -> !s.isBlank())
.map(String::trim)
.orElseThrow(() -> new InvalidDomainObjectError("Name of the addressee is invalid."));
}

private Locale validLocale(String languageTag) {
String tag = Optional.ofNullable(languageTag)
.map(String::trim)
.map(String::toLowerCase)
.orElseThrow(() -> new InvalidDomainObjectError("Locale of the greeting must not be null."));

return Arrays.stream(Locale.getISOLanguages())
.filter(code -> code.equals(tag))
.findAny()
.map(Locale::forLanguageTag)
.orElseThrow(() -> new InvalidDomainObjectError("Sorry, this language is not supported yet."));
}

}

The code above shows that we take care to construct an always-valid object with non-null values for addressee and locale. Moreover, we assure that only a language for one of the supported locales (ISO 639 alpha-2 code) is supported. In all other cases, an attempt to create a new Greeting will result in an InvalidDomainObjectError exception.

Output ports

Other than the name of the addressee, a typical greeting will consist of a phrase or a formula. This group of words would usually differ from one language to another. As would the exact place of the person’s name among the words of the formula. This brings us to the realization that in the process of issuing a greeting we must obtain a greeting formula translated into the language of the addressee.

The exact way this translation is implemented should not be our concern, at this point. This will depend on the exact capabilities our programming language with its ecosystem can provide for us. Right now, we just define an interface to some template retrieval/translation mechanism. This is an example of an output port.

public interface GreetingsFormulatorOutputPort {

String translatedGreetingsTemplate(Greeting greeting);

}

Another output port, which we would obviously need, is a port to present either a successful outcome or an exceptional outcome of our use case back to the user. This is the output port for our Presenter.

public interface SayHelloPresenterOutputPort {

void presentError(Exception e);

void presentGreeting(Greeting greeting, String greetingsFormula);
}

Use Case

We are now ready to implement our use case. This is the most important component from the standpoint of Clean Architecture. It must elaborate a step-by-step procedure by which issuing a greeting (by the system) will take place. An important point: this use case must not depend on anything other than our domain model (Greeting value object), the output ports we outlined above, and the input port which our use case implements (omitted below for brevity). Here is how our system will “say hello”.

@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class SayHelloUseCase implements SayHelloInputPort {

SayHelloPresenterOutputPort presenter;
GreetingsFormulatorOutputPort greetingsFormulatorOps;

@Override
public void issueGreeting(String addresseeName, String languageTag) {
Greeting greeting;
String greetingsFormula;
try {
// create a valid domain model object from input parameters
greeting = new Greeting(addresseeName, languageTag);

// obtain appropriate greetings formula, i.e. translated template
greetingsFormula = greetingsFormulatorOps.translatedGreetingsTemplate(greeting);

// successful outcome: present the greeting
presenter.presentGreeting(greeting, greetingsFormula);
} catch (Exception e) {
// exceptional outcome: present an error
presenter.presentError(e);
}
}
}

Here are the steps we perform in our use case to issue a greeting:

  1. Create a domain model object encapsulating a Greeting for a particular addressee name and a particular language tag provided as parameters to the use case method.
  2. Obtain an appropriate greeting formula template translated into the language of our domain model.
  3. Present the greeting and and the formula back to the user.
  4. If anything went wrong, present an appropriate error message back to the user.

UI and Vaadin Flow

We are now done with the two innermost layers of our application: “Domain Entities” and “Use Cases” layers. Directing our attention to the remaining outermost layers — “Interface Adapters” and “Frameworks” layers — we shall explain how and where the remaining components of our application fit in.

Spring Framework will help us with greeting template translation. It has an excellent support for message sourses based on ResoureBundle backed by properties files. We leave it to the reader to explore the details of our secondary adapter — GreetingsTranslator — it’s very straightforward.

What should interest us particularly, and for the rest of the article, is the other components we usually find at the left-hand side of a hexagon. We are talking about Controller, Presenter, ViewModel, and View as they are represented by Robert C. Martin (“Uncle Bob”) himself on a widely familiar diagram.

A UML diagram by Robert C. Martin showing Clean Architecture components and their relation to each other. It focuses especially on Controller, Presenter, ViewMode, and View components.
A diagram by Robert C. Martin (with small addition) present in some of his talks and his book on Clean Architecture.

All the components highlighted on the diagram above must be implemented within the context of Vaadin Flow framework. Even though we are talking about two distinct layers of Clean Architecture here, we allow Flow framework to influence our implementation of these components. We must be pragmatic.

The main idea — is not to fight Vaadin Flow here but to let it help us with the implementation as much as possible, all the while, maintaining clean separation of dependencies between the UI-related components.

Controller

By far the biggest challenge of trying to reconcile Vaadin Flow and Clean Architecture is finding a way to separate Controller from Presenter. In a typical Flow application a view will combine the functions of a controller and a presenter. It will handle routing, it will react to the UI-triggered events, and it will, of course, create or update the composition of of the view elements proper.

Ideally, in a Clean Architecture application, Controller is responsible for routing and reacting to the UI events. Its work ends after executing an appropriate use case depending on the nature of each UI event. So, after tweaking a usual Flow view, we are left with just this functionality in our controller.

@Tag("greeting-controller")
@Route(value = "")
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class GreetingController extends Component implements RouterLayout {

GreetingViewModel viewModel;

public GreetingController() {

// initialize view-model
viewModel = new GreetingViewModel(handleSayHello());

}

private ComponentEventListener<ClickEvent<Button>> handleSayHello() {
return event -> {
// update bound properties of the view-model
viewModel.update();
// execute the use case passing input parameters bound to the view-model
useCase().issueGreeting(viewModel.getName(), viewModel.getSelectedLanguageTag());
};
}

private SayHelloInputPort useCase() {
// get the use case from the context passing the view-model
return SpringUtils.getBean(SayHelloInputPort.class, viewModel);
}

@Override
public Element getElement() {
return viewModel.getViewElement();
}
}

We shall look at the view-model shortly. Here, let’s see what are the responsibilities of our controller.

  • Controller implements RouterLayout of Flow to be able to participate in the routing of requests.
  • Controller declares all the ComponentEventListeners for our application (in our case it’s just one —a listener for a button click event).
  • Controller then instantiates a view-model by passing all listeners to it.
  • Controller executes a method of a use case which it looks up from the application context. In doing so, it supplies the view-model instance it has instantiated previously.

Controller does not participate in a view creation or modification, in any way.

View

Before looking at the view-model, let’s quickly examine the view. Coming with no surprise here, we notice that GreetingView is a hierarchical structure of UI elements extending Flow’s VerticalLayout. In addition, there are just some helper methods which help toggling visibility of the related elements of the view.

@StyleSheet("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css")
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class GreetingView extends VerticalLayout {

// view elements which need package-private accessors (for binding from view-model)
@Getter(AccessLevel.PACKAGE)
TextField tf;
@Getter(AccessLevel.PACKAGE)
ComboBox<Language> cb;
@Getter(AccessLevel.PACKAGE)
Button btn1;

// view elements which will only be accessed locally from the view
Div greetingDiv;
Div greetingAlert;
Div errorDiv;
Div errorAlert;

public GreetingView() {
setMargin(true);

Div container = new Div();
container.setClassName("container");

Div row = new Div();
row.setClassName("row d-flex align-items-end");

tf = new TextField();
tf.setLabel("Name");
tf.setClassName("col-2");
tf.setValue("George");
row.add(tf);

cb = new ComboBox<>();
cb.setLabel("Language");
cb.setItemLabelGenerator(Language::getLabel);
cb.setItems(Language.values());
cb.setClassName("col-2");
cb.setValue(Language.English);
row.add(cb);

btn1 = new Button("Say hello");
btn1.setClassName("col-2");
row.add(btn1);

greetingDiv = new Div();
greetingDiv.setClassName("row mt-3");
greetingDiv.setVisible(false);

greetingAlert = new Div();
greetingAlert.setClassName("col-6 alert alert-info");
greetingAlert.setText("Hello World!");
greetingDiv.add(greetingAlert);

errorDiv = new Div();
errorDiv.setClassName("row mt-3");
errorDiv.setVisible(false);

errorAlert = new Div();
errorAlert.setClassName("col-6 alert alert-danger");
errorAlert.setText("Error");
errorDiv.add(errorAlert);

container.add(row, greetingDiv, errorDiv);
add(container);
}

// package private methods for view manipulation from the view-model

void showGreeting(String text) {
errorDiv.setVisible(false);
greetingDiv.setVisible(true);
greetingAlert.setText(text);
}

void showError(String text) {
greetingDiv.setVisible(false);
errorDiv.setVisible(true);
errorAlert.setText(text);
}
}

ViewModel

As we can see from the above diagram, our view-model is one object which ties it all together for us. As we have mentioned already, a view-model instance is created by a controller. Controller provides ViewModel with a set of event listeners for all possible actions of the UI represented by View. What are the main responsibilities of a view-model in this setup, then?

  • ViewModel creates a new instance of a view.
  • ViewModel registers listeners (provided by Controller) with some of the UI elements.
  • ViewModel binds some UI elements of the view to some of its own instance variables.
  • ViewModel provides some package-private getters for the values of the properties bound to some elements of the view (textbox, combobox, etc.). These values will be requested by Controller.
  • ViewModel provides some package-private methods for view manipulation as dictated by the needs of the presentation. These methods will be invoked by Presenter.
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class GreetingViewModel {

// Flow binder for this view-model
Binder<GreetingViewModel> binder = new Binder<>(GreetingViewModel.class);

// root element of the view
GreetingView view;

// bean properties bound to the view
@Getter
@Setter
@NonFinal
String name;

@Getter
@Setter
@NonFinal
Language selectedLanguage;

public GreetingViewModel(ComponentEventListener<ClickEvent<Button>> sayHelloHandler) {
view = new GreetingView();

// bind view elements to the view-model properties
binder.bind(view.getTf(), "name");
binder.bind(view.getCb(), "selectedLanguage");

// register event handlers with appropriate view elements
view.getBtn1().addClickListener(sayHelloHandler);
}

Element getViewElement() {
return Optional.ofNullable(view).map(VerticalLayout::getElement).orElse(null);
}

/**
* Updates this view-model by writing all values from the view into the corresponding
* properties.
*
* @throws IllegalStateException if there was an error binding the values of the view
*/
public void update() {
try {
binder.writeBean(this);
} catch (ValidationException e) {
throw new IllegalStateException(e);
}
}

/**
* Updates the view with the current values of the corresponding properties of this
* view-model.
*/
public void updateView() {
binder.readBean(this);
}

String getSelectedLanguageTag() {
return Optional.ofNullable(selectedLanguage).map(Language::getTag).orElse(null);
}

void showGreeting(String text) {
view.showGreeting(text);
}

void showError(String text) {
view.showError(text);
}

}

Presenter

We direct our attention now to the last component — Presenter. Presenter is often overlooked in an application. However it has a very specific set of responsibilities in Clean Architecture. Presenter is wired (or passed as a parameter at construction time) into Use Case and it serves as an adapter for the specific UI technology used to communicate all possible outcomes of a use case back to the user. In our example, Presenter will be used to communicate with Vaadin Flow framework (represented via ViewModel) to output a textual representation of a greeting in a selected language.

Often, in an application, locale-specific or language-specific formatting of the results of a use case is the job of Presenter itself. However, in our example, our “business logic” has precisely to do with creating a translated greeting message. So, in our case, the appropriate work is done in the use case. What is left to the presenter, is to actually construct the greeting message using the name of the addressee from the corresponding domain model and the greeting formula template passed to the presenter from the use case.

@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class SayHelloPresenter implements SayHelloPresenterOutputPort {
GreetingViewModel viewModel;

@Override
public void presentError(Exception e) {
try {
viewModel.showError(e.getMessage());
} catch (Exception ex) {
// should not happen
throw new IllegalStateException(ex);
}
}

@Override
public void presentGreeting(Greeting greeting, String greetingsFormula) {
try {
// format greeting message for the addressee
String greetingText = greetingsFormula.formatted(greeting.getAddressee());
// show greeting
viewModel.showGreeting(greetingText);
} catch (Exception e) {
// handle any errors
presentError(e);
}
}
}

Presenting a textual greeting message back to the user, at some point, will involve modifying the current view of the application: i.e. making an alert element visible on the screen. But the way this will be done exactly is not the concern of Presenter. Presenter delegates actual view manipulation to ViewModel since it is the only component with an access to View which Presenter itself has no knowledge of.

Flow of Control

Let’s recapitulate the flow of control through our simple application to bring home the important implementation moments.

  1. User types in a name and selects a language.
  2. Flow View registers the modification to the state of the view elements (which combobox item has been selected, etc.) for us.
  3. User clicks the “Say Hello” button.
  4. Controller reacts to the UI event (click) via the dedicated ComponentEventListener.
  5. Controller asks ViewModel to update the state of its view-bound properties in order to get access to the current values of the UI elements it is interested in.
  6. Controller decides which Use Case instance it must invoke passing any necessary parameters (the name, the selected language tag) to it.
  7. Use Case performes “business logic”. First, it constructs a valid domain object, enforcing any domain invariants (such as verifying if the selected language is supported).
  8. Then, Use Case proceeds to perform any necessary manipulation with the domain model (greeting formula translation) possibly calling any required output ports in the process.
  9. In a case of a successful outcome, Use Case finishes its work by calling an appropriate method of Presenter, passing it the results of “business logic” processing (a Greeting domain model and a translated greeting formula).
  10. Presenter creates an appropriate presentation from the results of a use case. In our example, it simply formats the greeting formula with the name of the addressee creating a suitable (textual) representation of the greeting to be displayed to the user.
  11. Presenter asks ViewModel to update View accordingly: reflecting either success or a failure (error) of the use case at hand.
  12. ViewModel manipulates required UI element of the Flow View accordingly.

Is this Over-Engineering?

Well, it depends. If the only function required of our program is as it is described in this article, and, moreover, if we are absolutely sure that no more requirements are coming, then, yes, this is definitively a form of over-engineering. But we all know how it goes. Our successful “Hello World!” application may grow into a application for our Linguistic Anthropology department where it will be used as tool in the research into greeting customs of the ancestral and contemporary societies. Before we know, we need to support many more languages conforming to different ISO standards and coming from various databases. We now have authenticated users with different preferred browser locales which we need to take into consideration when displaying greetings. We may even need to support different types of presentation since not all our languages are spoken or written. It is easy to imagine all the complexities which all these requirements will entail in terms of development and maintenance of our application.

In order to maintain a relatively sophisticated application, we must carefully consider how to balance the power of a UI framework such as Vaadin Flow with the discipline imposed on us by the precepts of Clean Architecture (and Domain-Drinven Design). Only then we can assure that the application will truly scale well, all the while, remaining easy to maintain, to test, and to understand.

References

  • Full source code of the example application
  • Article describing another excellent Java-based UI technology — ZK Framework — in the context of Clean Architecture

--

--