Hello World Dev.to!
This is my first post on dev.to, so let's start with the usual Hello World Dev.to! app, using Java and applying Clean Architecture (as per my understanding).
Our tiny application provides one single use case, with the following spec:
- inputs: name and country
- outputs: greeting message according to the country
-
steps:
- the user submits name and country 2-digit code
- the system fetch associated greeting word with the provided country code
- the system displays message by concatenating the provided name and fetched greeting word
-
exceptions:
- missing name: display error message
- unknown country: use English greeting
Clean architecture (briefly)
The Clean Architecture (CA) term was coined by R.C. Martin and consists of 4 concentric layers:
- Domain Business Logic (the innermost circle/layer)
- Application Business Logic
- Interface Adapters
- Framework & Drivers (the outermost circle/layer)
The number of layers/circles can be less or more than 4, but the most important rule is that no layer/circle can have access to an outer layer/circle.
For more details on CA, you can read this series on clean architecture and this detailed article.
In this post, the two outermost layers will be merged, and we will have the following layers (renamed):
- Domain (the Domain Business Logic)
- Use Cases (The Application Business Logic)
- Infrastructure (combined Interface Adapters and Framework & Drivers)
Hello Dev.to! application structure
A picture is better than thousands words, so here is our greeting application structure:
Domain and Use Case classes
Our domain is a just one class (of type record) to model a country.
public record Country(String code, String greeting){}
Our use case is materialized by 5 classes (1 implementation, 2 interfaces and 2 DTO) and uses 1 gateway interface (to interact with the persistence infrastructure). In total, we need 6 classes to code the use case.
Instead of having 6 java files (one for each class), we create just two files:
- one API file which encloses all interfaces and DTO
- one file for the use case implementation
GreetAPI.java, the enclosing file for all use case API code
public interface GreetAPI {
record InboundModel(String name, String country) {}
interface Inbound {
void execute(InboundModel request);
}
record OutboundModel(String greeting) {}
interface Outbound {
void success(OutboundModel response);
void failure(Throwable error);
}
interface Gateway {
List<Country> countries();
}
}
A user (or system) has to acquire a GreetAPI.Inbound and provides a GreetAPI.InboundModel instance in order to use our use case.
Once called, our use case will perform its service and give back result (or error) by calling GreetAPI.Outbound#success method or, in case of error, GreetAPI.Outbound#failure method.
The GreetAPI.Gateway provides the list of available countries from any implementing persistence back-end.
The real work is done by the GreetUseCase class which implements GreetAPI.Inbound interface.
GreetUseCase.java file
public class GreetUseCase implements GreetAPI.Inbound {
private final GreetAPI.Outbound presenter;
private final GreetAPI.Gateway gateway;
public GreetUseCase(GreetAPI.Outbound presenter, GreetAPI.Gateway gateway) {
this.presenter = presenter;
this.gateway = gateway;
}
@Override
public void execute(GreetAPI.InboundModel request) {
try {
if (request.name() == null || request.name().isBlank()) {
presenter.failure(new Throwable("Name is missing!"));
} else {
String greeting = gateway.countries().stream()
.filter(c -> c.code().equals(request.country()))
.map(Country::greeting)
.findFirst()
.orElse("Hello");
String msg = greeting.concat(", ")
.concat(request.name())
.concat("!");
presenter.success(new GreetAPI.OutboundModel(msg));
}
} catch (Exception ex) {
presenter.failure(ex);
}
}
}
We have now a fully functional greeting use case, with necessary interfaces to interact with any adapting infrastructure, and our first infrastructure will be test code!
Testing (or first infrastructure)
For testing purpose, we will use list based GreetAPI.Gateway implementation.
private final GreetAPI.Gateway gateway = () ->
Arrays.asList(
new Country("EN", "Hello"),
new Country("ES", "Hola"));
//this is our presenter (see next)
private final GreetPresenter presenter = new GreetPresenter();
An implementation will be also provided for the GreetAPI.Outbound, that captures the result (response or error) and provides it through GreetPresenter#get method.
public class GreetPresenter implements GreetAPI.Outbound {
GreetAPI.OutboundModel _response = null;
Throwable _error = null;
@Override
public void success(GreetAPI.OutboundModel response) {
_response = response;
_error = null;
}
@Override
public void failure(Throwable error) {
_response = null;
_error = error;
}
public Optional<GreetAPI.OutboundModel> get() throws Throwable {
if (_response != null) {//success
return Optional.of(_response);
}
if (_error != null) {//failure
throw _error;
}
return Optional.empty();//neither!
}
}
Three scenarios have been identified (as per the spec):
- non null name with existing country (happy path)
- null name (first exception case)
- non null name with unknown country code (second exception case)
Happy path
@Test
void validNameAndKnownCountry() throws Throwable {
GreetAPI.Inbound greet = new GreetUseCase(presenter, gateway);
greet.execute(new GreetAPI.InboundModel("Sancho", "ES"));
GreetAPI.OutboundModel response = presenter.get().get();
assertThat(response.greeting()).isEqualTo("Hola, Sancho!");
}
Missing (null) name path
@Test
void missingName() {
GreetAPI.Inbound greet = new GreetUseCase(presenter, gateway);
greet.execute(new GreetAPI.InboundModel(null, "ES"));
assertThatCode(presenter::get)
.hasMessageContaining("Name is missing!");
}
Unknown country code path
@Test
void unknownCountry() throws Throwable {
GreetAPI.Inbound greet = new GreetUseCase(presenter, gateway);
greet.execute(new GreetAPI.InboundModel("Dupont", "FR"));
GreetAPI.OutboundModel response = presenter.get().get();
assertThat(response.greeting()).isEqualTo("Hello, Dupont!");
}
Infrastructure details
It's time to plug our use case into concrete infrastructure.
For persistence, we will use and embedded H2 database containing one table (named country) with two columns (code and greeting).
Here is our gateway implementation.
GreetGatewayJdbc.java file
public class GreetGatewayJdbc implements GreetAPI.Gateway {
private final DataSource ds;
public GreetGatewayJdbc(DataSource ds) {
this.ds = ds;
}
@Override
public List<Country> countries() {
List<Country> countries = new ArrayList<>();
String sql = "select code, greeting from country";
try (PreparedStatement pstm = ds.getConnection().prepareStatement(sql)) {
ResultSet rs = pstm.executeQuery();
while (rs.next()) {
countries.add(new Country(rs.getString("code"), rs.getString("greeting")));
}
return countries;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
For user interface, we will use HTML, one page as form to collect name and country, another page to display the greeting. Instead of file on disk, string HTML will be used.
Two end point will be available:
- GET /greet (the form page)
- POST /greet (the greeting page)
The JDK Http Server (which is included with all jdk since version 6) is used as web server.
Here is our web based presenter. On success response, it renders a greeting HTML page and on failure, it renders an error HTML page with exception message. It uses the HttpExchange object provided by the Http Server.
GreetHtmlPresenter.java file
public class GreetHtmlPresenter implements GreetAPI.Outbound {
private final HttpExchange exchange;
public GreetHtmlPresenter(HttpExchange exchange) {
this.exchange = exchange;
}
@Override
public void success(GreetAPI.OutboundModel response) {
String html = """
<html>
<head><title>Greeting!</title></head>
<body><h1>%s</h1></body>
</html>
""".formatted(response.greeting());
try {
exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
exchange.getResponseBody().write(html.getBytes());
exchange.getResponseBody().close();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public void failure(Throwable error) {
String html = """
<html>
<head><title>Greeting!</title></head>
<body><h1>Internal error!</h1><h3>%s</h3></body>
</html>
""".formatted(error.getMessage());
try {
exchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, 0);
exchange.getResponseBody().write(html.getBytes());
exchange.getResponseBody().close();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
The HTTP based controller is defined below. It extracts form field values submitted by the user and call the use case.
GreetHttpController.java file
public class GreetHttpController {
private final HttpExchange exchange;
private final GreetAPI.Inbound useCase;
public GreetHttpController(HttpExchange exchange, GreetAPI.Inbound useCase) {
this.exchange = exchange;
this.useCase = useCase;
}
public void handle() throws IOException {
if(exchange.getRequestMethod().equalsIgnoreCase("POST")) {
String req = new String(exchange.getRequestBody().readAllBytes());
String[] params = req.split("&");
Map<String, String> values = new HashMap<>();
values.put(params[0].split("=")[0], params[0].split("=")[1]);
values.put(params[1].split("=")[0], params[1].split("=")[1]);
useCase.execute(new GreetAPI.InboundModel(values.get("name"), values.get("country")));
} else {
exchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0);
}
}
}
In order to make our web application fully functional, we need to create an HttpHandler that glues together our controller and presenter. This handler will also displays the form page.
GreetHandler.java file
class GreetHandler implements HttpHandler {
private final GreetAPI.Gateway gateway;
private GreetHandler(GreetAPI.Gateway gateway) {
this.gateway = gateway;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
String method = exchange.getRequestMethod().toUpperCase();
if ("POST".equals(method)) {
//display greeting page
GreetAPI.Outbound presenter = new GreetHtmlPresenter(exchange);
GreetAPI.Inbound useCase = new GreetUseCase(presenter, gateway);
new GreetHttpController(exchange, useCase).handle();
} else {
//display form to collect name and country
exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
exchange.getResponseBody().write(greetingForm.getBytes());
exchange.getResponseBody().close();
}
}
}
Greeting form html
String greetingForm = """
<html>
<head><title>Welcome</title></head>
<body>
<h2>Please provide your name and country code.</h2>
<form action='/greet' method='POST'>
<label for="name" style="display: inline-block; width: 70px">Name:</label>
<input id="name" name='name'/>
<br/><br/>
<label for="country" style="display: inline-block; width: 70px">Country:</label>
<input id="country" name="country"/>
<br/><br/>
<button type='submit'>OK</button>
</form>
</body>[](url)
</html>
""";
The application entry point
Finally, this is our static main().
public static void main(String[] args) throws IOException {
JdbcDataSource jds = new JdbcDataSource();
jds.setUrl("jdbc:h2:mem:data;db_close_delay=-1;init=runscript from 'classpath:script/init.sql'");
HttpServer server = HttpServer.create(new InetSocketAddress(8182), 0);
server.createContext("/greet", new GreetHandler(new GreetGatewayJdbc(jds)));
server.start();
}
}
Before configuring and starting the Http Server, a JDBC data source to an embedded H2 database is configured with an initialization sql script.
init.sql file
create table if not exists country(
code varchar(2) not null primary key,
greeting varchar(64) not null
);
delete from country;
insert into country (code, greeting) values ('EN', 'Hello');
insert into country (code, greeting) values ('ES', 'Hola');
After you run the entry point method, the application is available on http://localhost:8182/greet!
Top comments (0)