As you have seen, the major problems with monolithic applications are:
Inabilities to easily modify or maintain the application.
Inability to test individual components.
We’ve also seen the better path of splitting the application into layers. In this chapter, we’ll look closer at the communication between layers.
Set Up Your Communication Layers Right
To start, let’s look at how to connect the controller classes to our view layer and our data layer. In our case, Spring Boot establishes the communication and makes the various connections for us. The following approach is one you'll see applied over and over again. Here’s a piece of ClientController.java:
@RequestMapping("/clients/owe")
public String clientOwe( Model model) {
model.addAttribute("clients", clientRepository.findByOustandingBalanceGreaterThan(0.0));
return "clients";
}
Line 1: This is the connection between the display page (view), and the controller. It says that when the application decides to display the page /clients/owe, the function
clientOwe()
is called. Choosing to display this page is governed by the reference to it inclients.html
.Line 3: This is the connection between the controller and the data layer. It says to find the client's data model, then add all of the client data that matches the repository query. There is a clear separation between what the controller needs to know about a client and how that information is saved. All of the saving information is kept within the ClientRepository class. In this way, we can control (or filter) which clients are to be displayed by this page.
Line 4: Another connection between the controller and the view. This is telling Spring Boot what page to use for displaying the data obtained in the previous line.
By following this approach of having clear communications between the various layers, we can add different pages, with different filters very quickly. Let’s see how quickly you can do it!
Try It Out For Yourself
Add the functionality for a new use case: “Find all clients who owe a lot of money (defined as more than 150), so I can call them personally.”
If you need more guidance, here are the steps.
Add a method to the ClientController class.
@RequestMapping("/clients/owe-lots") public String clientOweLots( Model model) { model.addAttribute("clients", clientRepository.findByOustandingBalanceGreaterThan(150.0)); return "clients"; }
Add a button to the clients.html page.
<a href="/clients/owe-lots" class="btn btn-primary">Owe Lots</i></a>
Once you're ready, check out the solution in the GitHub project.
OK, it makes sense. So, how does this get messed up?
It gets messy when we forget the purpose of each layer. It is easy to try to make the controller layer do more than it should. For example, we could have made the clientOweLots
method do its own client filtering:
@RequestMapping("/clients/owe-lots")
public String clientOweLots( Model model) {
List<Client> clients = new ArrayList<>();
Iterable<Client> allClients = clientRepository.findAll();
for (Client client: allClients) {
if (client.getOutstandingBalance() > 150.0) {
clients.add(client);
}
}
model.addAttribute("clients", clients);
return "clients";
}
It's easy in the short term, but it creates a nightmare long term. Sometimes knowing how a layer is implemented can lead to poor messaging (communication). Remember, the data layer is responsible for managing the data. The logic should be kept there.
Design the Communication in a Data API
Currently, our application delivers all data wrapped inside a visual component. But what if we needed to deliver just the data portion? We’ll need to add an API to the outside world.
Design Considerations for APIs
There are several important questions we need to ask when designing a data API:
1) What is the intent of my API?
First, we need to build an API that is useful to our consumers (those that call the API). The easy option would be to expose all of our data as a set of CRUD operations. While CRUD functionality is useful, we want an API that exposes the intent of our application. As we saw earlier in the course, we need to go deeper than that and provide more focused functionality that aligns with our client's needs. We can, therefore, figure out our API's intent by revisiting our use cases.
2) What messages should I choose?
An API is defined by the messages it sends and receives. So choosing the messages is extremely important. We'll explore this a little further in the section below.
3) What data format do I need?
The format of the messages needs to be considered. We need flexibility. Fortunately, text-based messaging, through JSON or XML, allows for precisely that. Both mainly treat data as key/value pairs (it’s a little more complicated than that, but that’s a detail for another course).
While we can choose either one, JSON has become an industry-standard, so we’ll go with that for our API. Leveraging the Spring Boot framework will make the mechanics of putting an API into place rather simple. Message definition will then handled through the JSON that will be transferred back and forth.
Design Considerations for Messages in APIs
There are two areas of concern when it comes to determining an API’s messages.
Determine inbound and outbound data: What is going in, and what is coming back?
Evaluate future modifications: What is likely to change over time?
Determine Inbound and Outbound Data
To call an API, we first need to know what data to send. Here are three useful questions to start from:
What does the data look like?
What data is required? What is optional?
Are there ranges of acceptable values?
1) What does the data look like?
It's easy to create confusion with how you represent data. Here’s a simple one that can cause problems: representing dates.
For example, if the date is 2020-05-09, is it the 5th of September, or the 9th of May? We’ll have to make a decision one way or the other (YYYY-MM-DD, or YYYY-DD-MM) and document it, so anyone using the API knows which one we’ve chosen. JSON does not have a built-in date type, so this mistake is possible.
Think ahead to these kinds of issues. If possible, use agreed-upon standards (ex. UTC timecode) instead of choosing something on a whim.
2) What data is required? What is optional?
To illustrate this, we can look at the MaintenanceIssues
. When we initially set up that data, the entry date was mandatory, but the fixed date optional (it might not be fixed yet). When our API takes in this data, we’ll enforce those rules as well. JSON works well for this. When parsing the values, we can check if needed values are included, and reject the data if they are absent. If optional values are provided, they will be used.
3) Are there ranges of acceptable values?
Here, we can look at the MaintenanceIssues again. Each issue has a specific subsystem and severity. Our API will ignore messages that do not send the right kind of data for those items. Once again, JSON allows for this type of behavior, since data can be stored as a string. The string data can then be compared against a set of valid values.
Evaluate Future Modifications
One last consideration in designing our API is change. It’s likely that over time different data may be required or no longer needed. That’s another reason why JSON works well for managing the message data. Its flexibility allows for new items to be added, and old items dropped. If we separate the layer that deals with user interaction and upgrade our endpoints, maintaining backward compatibility is much easier than with a monolithic design.
Implement a Data API
Now let’s add an API to the maintenance portion of our application. The great part about using our existing framework is we have very little to do. We just need to add an endpoint controller. We don’t even need to generate an HTML page to show the results!
To add the new controller:
Add a new class
MaintenanceJSONController.java
in thecom.dairyair.dairyairmvc.controllers
package.Tell the framework that it is a rest controller, and the endpoint location.
@RestController @RequestMapping(path = "v1/maintenance") public class MaintenanceJSONController {
Connect the controller to the maintenance repository.
private final MaintenanceRepository maintenanceRepository; @Autowired public MaintenanceJSONController(MaintenanceRepository maintenanceRepository) { this.maintenanceRepository = maintenanceRepository; }
Add the actual endpoint handler.
@GetMapping(path="/", produces = "application/json") public Iterable<MaintenanceIssue> MaintenanceForm( Model model) { return maintenanceRepository.findAll(); }
When completed, the whole new set of functionality looks like this:
package com.dairyair.dairyairmvc.controllers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.dairyair.dairyairmvc.entities.MaintenanceIssue;
import com.dairyair.dairyairmvc.repositories.MaintenanceRepository;
@RestController
@RequestMapping(path = "v1/maintenance")
public class MaintenanceJSONController {
private final MaintenanceRepository maintenanceRepository;
@Autowired
public MaintenanceJSONController(MaintenanceRepository maintenanceRepository) {
this.maintenanceRepository = maintenanceRepository;
}
// localhost:8080/v1/maintenance/unfixed/
@GetMapping(path="/unfixed", produces = "application/json")
public Iterable<MaintenanceIssue> MaintenanceForm( Model model) {
return maintenanceRepository.findByFixed("");
}
}
Here’s a cool part about providing functionality through an API: it’s tough to mess things up! OK, it’s still possible. A telephone number can be provided, but have a typo in the data.
But otherwise, this is how an API can help prevent errors. If an API does not provide a certain capability, then that capability does not exist. For example, if there is no removeAllClients
endpoint, then all the clients can’t accidentally be deleted. As I said, it’s still possible. You could delete them all one at a time. 😉
Putting it all together, we’ve seen that the only knowledge that we need to have for a decoupled API is the JSON or XML descriptions of the data going in and the data coming back.
Let's Recap!
Respect the separation of responsibilities when implementing your different layers.
Interactions between layers should always occur through a well-defined API.
To ensure your data is well defined in your API:
Determine the intent and the data format.
For your messages between layers, define:
What your data will look like.
Which data is mandatory and which is optional.
What are the ranges of acceptable values.
Possible future modifications.
Now that we've looked at messages between layers via a well-defined API, let's check out how to choose the right communication style for our application!