If you’re beginner looking to learn how to develop web services using Spring Boot, or any other language/framework for that matter, you’ll more than likely spend your time following examples and tutorials you find on Google. These tutorials are generally great, but you need to remember they’re aimed at helping you to put something together that will give you results, there’s no guarantee that they’re showing the best way to build your web service.

In this article, I wanted to show you how a tutorial will more than likely describe how to generate a web service, then I want to discuss what issues you may have with the approach, and suggest a way to improve the approach by utilising a service layer and the Data Transfer Object (DTO) pattern.

Let’s start by first looking at a really basic web service endpoint, whose response returns an employees details. First, we’ll look at the Entity that maps to the database table;

@Entity
public class Employee {

    @Id
    @GeneratedValue
    private Long employeeId;

    private String firstname;

    private String lastname;

    private String emailAddress;

    private String department;

}

It’s a very simple entity, each property maps to a column in a MySQL database. Next, we need a way to return an instance of the entity as a JSON response to the client calling the endpoint. To do this, we’ll create a very simple RestController that grabs an employee from the database, and returns it as a response.

@RestController
@RequestMapping("/employee")
public class EmployeeController {

    private EmployeeRepository employeeRepository;

    @Autowired
    public EmployeeController(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @RequestMapping(value = "/{employeeId}", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity getEmployee(@PathVariable("employeeId") Long employeeId) {
        Employee employee = employeeRepository.find(employeeId);

        return new ResponseEntity(employee, HttpStatus.OK);
    }

}

Above is the approach I have witnessed many tutorials follow. For the sake of simplicity, I’ll not include the code for the EmployeeRepository as it doesn’t have any relevance to what I’m trying to discuss, but you should know that the EmployeeRepository is responsible for accepting an employeeId, retrieving the employee from the database, mapping it to the Employee entity and returning it to the method (Controller) that requested it. You see our endpoint is very simple, a call to /employee/12345 will produce a JSON response that contains the details of the employee if it was found. The JSON response would look like this;

{
	"employeeId": 12345,
	"firstname": "Harry",
	"lastname": "Davidson",
	"emailAddress": "hd@example.com",
	"department": "Finance"
}

Fantastic, now using this knowledge you can build your web service. Ok, it’s a very basic web service, using this approach would certainly work, so what’s wrong with it?

In the above example, we’ll assume that the web service is being used to pass data to a mobile application. The owner of the application has decided that a new feature is required, and the app now needs to handle an employee having multiple email address. That’s easy you think, just change the response to return the email address as a JSON array. Wait… the response from the API needs to be backwards compatible with old version of the app too… ah… now we’ve hit a snag, but it’s still possible. Let’s update our entity to handle multiple email address;

@Entity
public class Employee {

    @Id
    @GeneratedValue
    private Long employeeId;

    private String firstname;

    private String lastname;

    private List<String> emailAddresses;

    private String department;

}

We should also create a new endpoint on our web service, so there’s an endpoint for the old version of the app, and another for the new version of the app, this way each mobile client receives the expected response. We’ll update the original endpoint so that it still only returns one email address, specifically the first email in the List (obviously you’d check it exists first in production).

@RequestMapping(value = "/v2/{employeeId}", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity getEmployeeV2(@PathVariable("employeeId") Long employeeId) {
        Employee employee = employeeRepository.find(employeeId);

        return new ResponseEntity(employee, HttpStatus.OK);
    }

    @RequestMapping(value = "/{employeeId}", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity getEmployee(@PathVariable("employeeId") Long employeeId) {
        Employee employee = employeeRepository.find(employeeId);

        Map<String, Object> employeeRsp = new HashMap<String, Object>();
        employeeRsp.put("employeeId", employee.getEmployeeId());
        employeeRsp.put("firstname", employee.getFirstname());
        employeeRsp.put("lastname", employee.getLastname());
        employeeRsp.put("emailAddress", employee.getEmailAddresses().get(0)); // Get the first email address

        return new ResponseEntity(employeeRsp, HttpStatus.OK);
    }

So, now we have two api endpoints, /employee/12345 which returns the old response, and /employee/v2/12345 which returns the new response. Great, we’ve solved the problem.

The problem with the above approach is that although it solves the problem, it’s not very flexible and is likely to cause maintainability issues in the future. What if we want to return employee details in multiple endpoints, it means we have to continually repeat the approach above. Let’s take a look at improving the approach.

Service Methods and DTO’s

In our example, we’ve accessed the EmployeeRepository directly within the Controller, which is bad practice, we’re coupling the presentation layer (the controller) with the source of data. Let’s say that as well as the /employee/12345 endpoint returning the employees details, it should also show the number of available holidays the employee has remaining, and this information is stored in another entity/table. We could edit our Controller methods to interact with other repositories, grab the additional data we need and then add it to the existing response. It solves the problem, but we’re coupling too much logic to the controller, it’s just not a flexible or maintainable approach. Instead, we should create a service layer which sits between our Controllers and Repositories. As well as the service layer, we’ll also introduce a Data Transfer Object (DTO). Here’s what our new Service class looks like;

@Service
public class EmployeeService {

    EmployeeRepository employeeRepository;    
    AnnualHolidayService annualHolidayService;
    
    @Autowired
    public EmployeeService(EmployeeRepository employeeRepository, AnnualHolidayService annualHolidayService) {
        this.employeeRepository = employeeRepository;
        this.annualHolidayService = annualHolidayService;
    }
    
    public EmployeeDTO getEmployee(Long employeeId) {
        Employee employee = employeeRepository.find(employeeId);
        
        return new EmployeeDTO(employee, annualHolidayService.getHolidaysRemaining(employee));
    }

}

There’s a few things we need to cover. The first is we’ve injected both a EmployeeRepository and an AnnualHolidayService into this Service. From this point onwards, our controllers are now allowed to interact directly with repositories, so we’ve created a getEmployee method on the Service that is now responsible for getting and returning an employee. The AnnualHolidayService, is another service which I’ve not included, but this service would have a method that accepts an Employee, and returns the number of holidays that employee has remaining. The final thing to notice is our new EmployeeDTO object which gets returned as the response, and this is what it looks like;

private Long employeeId;
    private String firstname;
    private String lastname;
    private String emailAddress;
    private String department;
    private int holidaysRemaining;

    public EmployeeDTO(Employee employee, int holidaysRemaining) {
        this.employeeId = employee.getEmployeeId();
        this.firstname = employee.getFirstname();
        this.lastname = employee.getLastname();
        this.emailAddress = employee.getEmailAddress();
        this.department = employee.getDepartment();
        this.holidaysRemaining = holidaysRemaining;
    }

You can see, it contains the employees details, and includes the number of holidays the employee has remaining. Wouldn’t it be great if we could return this as our web service response. Well, we can, here’s our updated method from our Controller;

@RestController
@RequestMapping("/employee")
public class EmployeeController {

    private EmployeeService employeeService;

    @Autowired
    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @RequestMapping(value = "/{employeeId}", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity getEmployee(@PathVariable("employeeId") Long employeeId) {
        EmployeeDTO employeeDTO = employeeService.getEmployee(employeeId);

        return new ResponseEntity(employeeDTO, HttpStatus.OK);
    }

}

It’s good practice to not return Entities directly from your Controller, hence the use of a Data Transfer Object. How you structure your database may not necessarily be the same way you’d structure a view in an application, a DTO allows you to stick with your database structure for data, and provide a different structure for you application, mainly web service responses. In the above example, we’ve moved code from the Controller to the Service, which means we can can call the Service methods from ANYWHERE in our application and we only need to control the logic in a single place, if we need to get an employee from a different controller, we can, and because the logic for this is handled in a single place, we don’t need to worry about the results being different. With this approach, your controllers should never need to see an entity, because in a lot of cases the response from your web service is unlikely to reflect the structure of your entities/database. The approach adds another layer to your application, but provides way more flexibility, makes your application more maintainable and more expandable.

What about the extra feature we discussed earlier, how would we approach that problem? Firstly, we update the getEmployee method in our EmployeeService so it takes into account our new data structure, but still returns the same structured response as it did before, and we need to add a new method to generate a new DTO object for our new response that will contain multiple email addresses. Our Service class now looks like this, one method for the old response, and a second to handle the new response.

@Service
public class EmployeeService {

    EmployeeRepository employeeRepository;
    AnnualHolidayService annualHolidayService;

    @Autowired
    public EmployeeService(EmployeeRepository employeeRepository, AnnualHolidayService annualHolidayService) {
        this.employeeRepository = employeeRepository;
        this.annualHolidayService = annualHolidayService;
    }

    public EmployeeV2DTO getEmployeeV2(Long employeeId) {
        Employee employee = employeeRepository.find(employeeId);
        int noOfHolidaysRemaining = annualHolidayService.getHolidaysRemaining(employee);

        return new EmployeeV2DTO(employee, noOfHolidaysRemaining);
    }

    public EmployeeDTO getEmployee(Long employeeId) {
        Employee employee = employeeRepository.find(employeeId);
        int noOfHolidaysRemaining = annualHolidayService.getHolidaysRemaining(employee);

        return new EmployeeDTO(employee, noOfHolidaysRemaining);
    }


@RestController
@RequestMapping("/employee")
public class EmployeeController {

    private EmployeeService employeeService;

    @Autowired
    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @RequestMapping(value = "/v2/{employeeId}", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity getEmployeeV2(@PathVariable("employeeId") Long employeeId) {
        EmployeeV2DTO employeeDTO = employeeService.getEmployeeV2(employeeId);

        return new ResponseEntity(employeeDTO, HttpStatus.OK);
    }

    @RequestMapping(value = "/{employeeId}", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity getEmployee(@PathVariable("employeeId") Long employeeId) {
        EmployeeDTO employeeDTO = employeeService.getEmployee(employeeId);

        return new ResponseEntity(employeeDTO, HttpStatus.OK);
    }

}

I’ve added the new V2 method to our existing controller, but if you were taking versioning seriously, you could create a new package labelled v2, create a new EmployeeController inside it that extends your original EmployeeController, and finally override the methods that you need to change, for example;

@RequestMapping("/v2/employee")
public class EmployeeV2Controller extends EmployeeController {

    @Autowired
    public EmployeeV2Controller(EmployeeService employeeService) {
        super(employeeService);
    }

    @RequestMapping(value = "/{employeeId}", method = RequestMethod.GET, produces = "application/json")
    @Override
    public ResponseEntity getEmployee(@PathVariable("employeeId") Long employeeId) {
        EmployeeV2DTO employeeDTO = employeeService.getEmployeeV2(employeeId);

        return new ResponseEntity(employeeDTO, HttpStatus.OK);
    }

}

If our original EmployeeController contained methods for deleting an Employee, or listing Employees, the V2 version of our EmployeeController would inherit those methods, and make them available under /v2/employee endpoints. You could follow the same approach for the EmployeeService too, create an EmployeeV2Service that extends EmployeeService and @override the getEmployee method, then inject this into your EmployeeV2Controller.

From our initial code, you can see that we’re reduced the amount of work our controller has to do. We’ve decoupled our application and assigned specific roles to our Controller and Service classes. There are many more design patterns we can apply to web services to make them awesome to work on, we’ve covered only a fraction of the great things we can do.