Building better GUIs using DDD and Spring HATEOAS
Oliver Drotbohm, the former project lead of Spring Data once said “Don’t poke with getters and setters at your entities”. ‘Entity’ thereby refers to a concept from domain-driven design for an object that models a stateful part of a business application.
Spring makes it however easy to do exactly that: By defining an interface annotated with @RestRepositoryResource
which extends Spring’s CrudRepository
, you can post JSON-Data against the REST-endpoint generated by Spring to update the entity via its setters with almost no code. Is that a design error of the Spring framework?
In this post, I would like to walk you through a concrete example of how we can implement a domain model in Java that only allows well-defined state transitions according to the business rules, then publish it using Spring in a REST-API that deserves its name, and see how we can build a simple web-application which is fully driven by that API. The implementation uses concepts from domain driven design (DDD), an approach where the software artifacts try to be congruent with the business model, e.g. by using terms of the domain language.
The problem with CRUD applications
The create-read-update-delete or short CRUD approach is so easy to implement that it is the primary approach in stateful web-applications to integrate the graphical user interface (GUI) with the backend. As a result, the entities in the backend merely model the state of the domain logic which needs to be persisted, but not the business operations and business rules that apply to produce valid state changes.
In the best case, there are service operations giving those operations a name and containing logic to implement the required business rules. These rules easily leak from there throughout the code base, and are all too often also found in the graphical user interface, nowadays usually implemented as a single page web-application running in a browser.
As a result, we have an application with what Martin Fowler calls an anemic domain model, and an intertangled ball of mud which is hard to understand and maintain, as responsibilities are not clearly separated. Based on heuristics, the GUI makes assumptions on what can be done with an entity and implements navigation logic which is decoupled from the backend.
HATEOAS and the Richardson maturity model
Going into literature to find solutions addressing this problem, you may come across the Richardson maturity model of HTTP application APIs. It starts at its base with plain old XML, meaning that XML content is posted to a web-service endpoint.
At level 1, the API introduces the concept of resources, allowing to manipulate entities of the backend individually and thus breaking a large service endpoint down into multiple resources.
A level 2 API makes further use of specific HTTP verbs like PUT
, DELETE
, or PATCH
to refine what the meaning of an operation is. Martin Fowler calls it providing a standard set of verbs so that we handle similar situations in the same way, removing unnecessary variation.
At level 3, discoverability is baked into the API by adding context-specific hyperlinks to each response, giving the client insights on what operations could be done next with a given resource, or linking related resources. At this level, the ‘hyper-text’ exchanged with the API acts as the engine of application state, rooted in the backend and revealed to the client - in our case the web frontend - via hyperlinks.
By the way, you just read over a sentence containing HATEOAS: hypertext as the engine of application state. In the terminology of the http protocol, hypertext refers to a document that contains links to other related documents, a concept that is the foundation of the success of the world-wide web. In the case of a REST-ful API, hypertext is usually a JSON document containing links.
One simple standard to express such links is the hypertext application language HAL. It defines an element called _links
which can be added to any resource. (See section Repository and basic REST API for an example.)
How can a REST-ful API and the HAL standard help us tackle our initial problem to encapsulate business logic in the backend? Let’s look at a concrete example.
Sample Domain: Manufacturing resources planning
Assume we are building a manufacturing resources planning system (MRP) where a product manager can prepare and submit a production order. The manufacturer can then accept the order, indicating the anticipated date of delivery, and complete the order once the goods have been produced.
In addition, the following business rules apply:
- The product manager cannot change a production order once it has been submitted.
- When the manufacturer accepts a production order, he must indicate a date in the future when completion of the order can be expected.
Figure 2 shows the respective domain story.
Setting up the project with Spring and Angular
As Josh Long keeps telling us, every project should start at https://start.spring.io. Indeed, the page is very handy and allows us to easily bootstrap a new application which includes the desired technology. For our case, we choose the following dependencies:
- Spring Data JDBC: A straight-forward OR mapper based on the java database connectivity (JDBC), sparing us from the overhead of JPA and very suitable for persisting DDD-style aggregates
- H2 Database: A relational database written in Java which can be run in-memory out of the box
- Rest Repositories: A Spring library which allows to publish our aggregates as REST resources
- Lombok: A bytecode generator which drastically reduces the amount of boilerplate code and gives Java a touch of modern languages like Kotlin or TypeScript
- Spring Boot DevTools: A development dependency which automatically restarts the application on every change in the code base
The downloaded ZIP can be extracted to a workspace and provides a fully functional Spring Boot application which can be started via the following command:
mvn spring-boot:run
The Angular frontend can be prepared with the following commands:
cd <project-root>/src/main
ng new frontend --defaults --skip-git=true
Adding a proxy configuration allows us to access the API of the Spring application served on localhost:8080 from within the Angular application running on port 4200 during development. The proxy will redirect every call against http://localhost:4200/api to http://localhost:8080/api. To do so, create a file src/main/frontend/proxy.config.json with the following content:
{
"/api/*": {
"target": "http://[::1]:8080",
"secure": false
}
}
Then adapt the file src/main/frontend/package.json as follows:
"start": "ng serve --proxy-config=proxy.config.json"
We can now start the frontend in development-mode using the following command:
npm start
The aggregate: where state meets business rules
At the core of a domain-driven application lives the domain model. It is free of technology and integration aspects and tries to follow the business model and terminology as closely as possible. The state of the application is thereby captured in so called entities, where related entities can be grouped to form an aggregate. Each aggregate defines a boundary of consistency inside the application, meaning that only well-defined state changes happen within the aggregate in a transactional fashion.
To keep focused, let’s start with a very simple model for our production order, with no child entities and only four fields:
id
: An identifier to distinguish different production orders. For simplicity, we model it as aLong
and let the database initialize it. The@Id
annotation tells Spring Data JDBC that this is the primary key.name
: The name of the production order. Once submitted, the name must not change anymore.expectedCompletionDate
: A date provided by the manufacturer when accepting the production order, indicating the planned completion of the manufacturing processstate
: The state of the production order according to the domain model. It can assume the valuesDRAFT
,SUBMITTED
,ACCEPTED
,COMPLETED
, modelled as an enumeration.
We annotate the class with Lombok’s @Getter
annotation, which generates the bytecode to instrument our (trivial) aggregate with getters to access the values of these fields from outside.
How can we now ensure that the domain model only allows well defined state transitions, as opposed to exhibit all fields via setters? The answer is: by implementing respective business operations. Let’s start with the first one: create
(see Listing 1).
Of course, we could use a constructor to create our entity. I prefer however providing a static factory method that allows us to use an appropriate business term as name instead of the rather technical new
-keyword. The create
-method takes the name of the production order as single argument and initializes the state to DRAFT
. The id
field will later be initialized by the framework when the aggregate is persisted to the database.
Listing 1: The initial aggregate containing the ‘create’ method.
Repository and basic REST API
To persist our aggregate to the DB and retrieve it from there, we define an interface which for simplicity extends the CrudRepository
interface of Spring Data. Instead of using “Repository” in its name, we call it ProductionOrders
to stick to the ubiquitous language of our domain.
By annotating this interface with @RepositoryRestResource
, Spring does not only provision us with the repository implementation, but also provides a full-blown REST API, where our aggregate is exposed as resource “productionOrder”.
Listing 2: The projection orders repository.
Starting the application and querying its API through the curl command line tool, we get the following response:
$ curl http://localhost:8080/api
{
"_links" : {
"productionOrders" : {
"href" : "http://localhost:8080/api/productionOrders"
},
"profile" : {
"href" : "http://localhost:8080/api/profile"
}
}
}
You notice the field _links
? Yes, Spring Data REST produces responses in HAL format by default! It reveals to us, that the API provides a collection resource “productionOrders”, including a link how to navigate there. If every resource provides all the links that a client needs to navigate to related resources and to invoke actions, then we come to the following observation:
A client needs to know one URL only, which is ‘/api’. In a truly REST-ful API, all other URLs can be retrieved from the responses of the API.
To further illustrate this concept, we create and persist some production orders in the DemoApplication
class, and follow then the href-Property of the productionOrders resource:
$ curl http://localhost:8080/api/productionOrders
{
"_embedded" : {
"productionOrders" : [ {
"name" : "Order 1",
"expectedCompletionDate" : null,
"state" : "DRAFT",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/productionOrders/1"
},
"productionOrder" : {
"href" : "http://localhost:8080/api/productionOrders/1"
}
}
}, {
"name" : "Order 2",
"expectedCompletionDate" : null,
"state" : "DRAFT",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/productionOrders/2"
},
"productionOrder" : {
"href" : "http://localhost:8080/api/productionOrders/2"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/productionOrders"
},
"profile" : {
"href" : "http://localhost:8080/api/profile/productionOrders"
}
}
}
We see that the two production orders are returned, contained in a property of the special field _embedded
defined by the HAL specification. If interacting with a resource requires additional information, like for example values for a dropdown list to filter for production orders in a certain state, this data could be added to another field under the _embedded
property of the response.
Each production order resource provides a set of links, which by the default is rather trivial: a self-link and a productionOrder link, both pointing at the resource itself. As a next step, we will now add business operations to the ProductionOrder
class and expose the endpoints to invoke them as additional links.
Adding business operations
If we recall the outline of the business requirements, our aggregate needs to support the following operations:
- Allow to rename when in status
DRAFT
- An operation to submit an order to the manufacturer
- An operation to accept the order, providing the expected delivery date
The implementation follows the approach we already used in the create method. Instead of providing getters and setters, we implement methods with names adhering to the language of our domain: renameTo
, submit
, accept
. As said above, the aggregate is seen as a boundary of consistency. Since the additional methods are not static anymore, we can directly make use of the fields of the class to enforce required business rules and grant that only well-defined state transitions are performed. For example, we can be completely sure to never encounter an accepted production order without completion date, as required by our second business rule. What a difference compared to a setter-based approach!
Listing 3: Implementation of the three business operations in the production order aggregate, allowing only well-defined state transitions.
Exposing business capabilities in the REST API
As a next step, we want now to expose the business operations in the REST API. The ingredients we need to do so are:
- New endpoints for each action of the form
/api/productionOrders/{id}/{action}
- Links in the HAL representation of the productionOrder resource
When you think about it, wouldn’t it be nice if we only exposed a link to the endpoint, if the respective action is actually permitted depending of the state of a given production order? This can be easily achieved as follows.
We first implement a ProductionOrderController
class and map it on class level to the /api/productionOrders
endpoint (see bug https://github.com/spring-projects/spring-data-rest/issues/1342 if you experience trouble with the mapping). This allows us to extend the standard API provided by Spring Data REST with additional methods: rename
, submit
, accept
. These methods take the production order ID from the path and any additionally required arguments from the request body. Since the integration with web technology is a concern of the application layer, we put it in the sub-package web
to clearly separate it from the domain logic.
The pattern for applying an action on the aggregate is always the same: load the aggregate from the persistent storage, invoke the business operation, and save it back to the store. This works likewise with relational persistence as in our case, but also with an event sourced model. For simplicity we do here everything in the controller, whereas in a larger or more puristic application the controller would delegate to a domain service.
The more interesting part with respect to the topic of this blog post comes by implementing the RepresentationModelProcessor
interface of Spring HATEOAS. In the method process
, it takes a production order wrapped as an entity model. This entity model allows adding additional links to a productionOrder resource. Because the model also provides the production order itself, we can easily check its state and then decide whether to generate a specific link. Spring provides the static helper methods linkTo
and methodOn
to dynamically derive the URL of the referenced controller method.
Listing 4: The custom REST endpoints to invoke the business operations on the production order aggregate, plus the logic to generate or omit related links.
Querying the productionOrders resource again provides us with the new links on each production order resource:
$ curl http://localhost:8080/api/productionOrders
{
"_embedded" : {
"productionOrders" : [ {
"name" : "Order 1",
"expectedCompletionDate" : null,
"state" : "DRAFT",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/productionOrders/1"
},
"productionOrder" : {
"href" : "http://localhost:8080/api/productionOrders/1"
},
"rename" : {
"href" : "http://localhost:8080/api/productionOrder/1/rename"
},
"submit" : {
"href" : "http://localhost:8080/api/productionOrder/1/submit"
}
}
}, {
"name" : "Order 2",
"expectedCompletionDate" : null,
"state" : "DRAFT",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/productionOrders/2"
},
"productionOrder" : {
"href" : "http://localhost:8080/api/productionOrders/2"
},
"rename" : {
"href" : "http://localhost:8080/api/productionOrder/2/rename"
},
"submit" : {
"href" : "http://localhost:8080/api/productionOrder/2/submit"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/productionOrders"
},
"profile" : {
"href" : "http://localhost:8080/api/profile/productionOrders"
}
}
}
You see that Spring does not expose the ID property of the aggregate. We will later see that we don’t need to know it in the client, because it is contained in the links.
Also note that each link has a relation property, short “rel”. This property is very important, as it defines the contract with the client of the API which links exist for a specific resource. We will see that shortly; our backend is now complete and we can move on to make use of it in the frontend.
Consuming the HAL model in the frontend
As I said before, the client of a true REST-ful API should only know one URL: /api
. Any other URL invoked by the client should then be derived from the links contained in the responses.
It would be a natural choice to emit a request for the base URL from within the top-level component of our Angular application, which is the AppComponent
, and pass it from there as input to child components. To keep it simple, we do here everything in the ProductionOrderListComponent
, fetching the API as part of the onInit
method and storing the response in the field root
(see listing 5).
To display the production orders, we add an HTML template to the @Component
-decorator at the top of the class and load the productionOrders from the backend by following the link with the relation “productionOrders” in the root resource. As we saw above, the url property of this link is http://localhost:8080/api/productionOrders, but the frontend is agnostic of that. In fact, the backend could serve the production orders under a completely different URL and our frontend would still work! Only the relation “productionOrders”, which is the contract between backend and frontend, must remain stable.
Listing 5: The initial production order list component, loading first the API resource, followed by loading the productionOrders via the URL provided under the respective relation.
With that code, we can start the frontend typing npm start
on the command line and pointing our browser at http://localhost:4200.
Next, we need to add a way that a user can perform the respective business actions on the model. The simplest way to do so is to add a button next to the production order for each action that is currently allowed.
Listing 6: Adding buttons to each production order, which are shown or hidden depending on presence or absence of the related link relation in the production order resource.
To decide whether a given action is allowed and the respective button should be shown, we query the underlying resource for a link with the given relation. In fact, we simply implement a feature toggle: if the relation is provided as a link, the action is enabled, else it is hidden in the GUI.
Note that the sample code (you find the link at the end of this post) adds a few interfaces to allow for type-safe access to the _links
-property of the production order resource.
Finally, we just need to add special treatment for the actions that require submission of additional data. Here again, we use the simplest solution using the native prompt-control to either accept the new name in case of the rename
action, or the expected completion date in the case of an accept
action.
Listing 7: A very simplistic implementation of the methods ‘can’ and ‘do’, making use of the relations and links or the production order resource.
Because we checked for the presence of a link in the can
-method, we can easily identify the URL where to post the body to by extracting the href-property of that link. Et voilà, also the frontend of our little demo application is complete.
What did we get from it?
With the chosen design, we established a well-defined interface between backend and frontend as depicted in the figure below. The contract includes the URL of the API root, the name of the productionOrders resource, plus the three actions rename
, submit
, accept
.
Where to go next
Use Spring Security to control which links to generate
In a real world application, we would now add Spring security to let users authorize themselves. We could then easily extend the logic in the controller to
- override the
GET /api/productionOrders
endpoint to suppress production orders in statusDRAFT
if the user had roleMANUFACTURER
- generate the submit link only if the user has role
PRODUCTMANAGER
- generate the accept link only if the user has role
MANUFACTURER
With these changes in the backend, we wouldn’t even have to change a single line of code in the frontend, but would achieve, that a user only sees a button if she is allowed to invoke the related action.
Build the menu dynamically based on resources
If the application adds more functionality in terms of new top-level resources, the frontend could use the root resource to dynamically build the application menu using the same feature toggle approach. If for examples a “settings” resource is exposed as link in the root resource, an admin menu could be generated, but would only be available if the user had role ADMIN
.
Additional media types
The HAL media type is only concerned with embedded resources and with links. How does a client know which http verb (like GET
, PUT
, POST
, PATCH
) is required to interact with a link? For the use case described above, one could argue that the http verb for invoking a business action must always be POST, as the request typically changes the state in the backend and is not necessarily idempotent. Submitting an already submitted production order in our sample code would for example lead to an exception.
The HAL specification proposes to solve this problem via additional documentation provided by the API. Spring Data REST by default includes a basic profile resource for each exposed entity. For our sample, the URL is http://localhost:8080/api/profile/productionOrders. If you invoke it you see that we would need to provide additional documentation to include our custom methods.
In addition, Spring HATEOAS supports various additional media types. One of them is HAL-FORMS, which is related to HAL, but provides meta information on the aggregates served by the API suitable to dynamically generate input forms, including form validation rules.
Conclusion
Although many developers know the Richardson maturity model for REST-ful APIs, only few of them use embedded links to drive application state. API documentation based on Open API (formerly known as Swagger) puts the focus on absolute URLs instead of relations. Therefore, frontend code is all too often tied to fixed URLs, and replicates much logic that would better reside in the backend.
Spring HATEOAS provides all the necessary tools for leveraging the full potential of REST. HAL is thereby a simple but powerful standard for link relations that can easily be consumed by an Angular application, being well suited to selectively publish business operations of a domain model that follows principles of domain driven design. The approach shown in this article could help significantly reducing the complexity in web applications, keeping business logic away from the frontend and make the frontend behave “just right” based on the state in the backend.
You can find the source code here: https://github.com/sth77/spring-angular-ddd-hateoas.