Cocktail Recipes, A Clean Architected Spring Boot App—Get a Cocktail Part 1

Ben Ashwell
5 min readJun 30, 2023

--

Spring Boot — Starting a project

To gain a better understanding of restricting Spring usage within the application’s boundaries, I’ll begin by setting up a Spring Boot project and creating a dedicated repository for my Cocktail Recipe API.

  1. Created a GitHub repository for the Cocktail Recipe API to go in — https://github.com/baashwell/cocktail-recipes
  2. I have created a Spring Boot application using the Spring Initializr https://start.spring.io/ and I chose maven (3.1.1) and Java (17). I did toy with Kotlin but the purpose of this part of the application is not to learn a new language. I also added Lombok, Spring Boot Dev Tools and Spring Web as dependencies.
  3. I pushed that generated code into the repository — a starting point.

The task at hand

This first step in keeping spring from creeping into my code base is to create an API that can be used to get a cocktail by a given ID. The cocktails returned will be from a static set of data to start with but this should give the basis of a good clean architected project.

Get Cocktail by ID API

For anyone unfamiliar with clean architecture the picture above may look a bit over-engineered for a simple REST Get API, and there is some truth in that. However, the benefit of building this way allows us to start with decoupling at the heart of our architecture.

You can see in the image the StaticCocktailRecipe Gateway to provide the GetCocktailByIDRecipe use case with the data it needs about the requested cocktail. When I decide to move from static data storage to an external data source such as a relational database I can simply swap to a different implementation of this gateway.

Tests approach

Before diving into development, I want to establish a testing approach that can evolve alongside the project.

From work in my current project I have come to love Double Loop TDD — meaning you write a failing acceptance test, then write unit tests to get to a passing acceptance test.

This approach gives confidence at an individual unit level and at a user journey level especially when combined with using a DSL layer.

I will show you the acceptance test here and if needed may do a deeper dive into this topic at a later time.

An acceptance test

As all good TDD enthusiasts do I began with a test. This test, in domain language, specifies that when retrieving a cocktail named ‘mojito,’ all the expected information should be returned.

package uk.co.benashwell.cocktailrecipe.acceptance;

import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import uk.co.benashwell.cocktailrecipe.acceptance.actors.HomeCocktailMaker;
import uk.co.benashwell.cocktailrecipe.web.CocktailController;


@WebMvcTest(controllers = CocktailController.class)
class GetACocktailTests {

@Autowired
private MockMvc mockMvc;

private HomeCocktailMaker homeCocktailMaker;

@BeforeEach
public void setup() {
homeCocktailMaker = new HomeCocktailMaker(mockMvc);
}

@Test
@DisplayName("GIVEN I am a home cocktail maker" +
" WHEN I get a cocktail recipe for a Mojito" +
" THEN I can see the recipe for a Mojito")
void getARecipeForAMojito() throws Exception {
homeCocktailMaker.getCocktailRecipe("Mojito");
assertTrue(homeCocktailMaker.canSeeCocktailRecipe("Mojito"));
}
}

As you might expect, this test fails currently as there is no API available. I have created our actor, HomeCocktailMaker which is our DSL, I am not 100% happy with how much spring is leaking into these tests and think that maybe I could create another bean that obscures this.

If there are a lot of questions about the Acceptance test approach I can do a follow-up post around this.

Create an API

Now that I have a failing acceptance test, I can start making changes to pass the test. The first step is to create an API that our acceptance test currently interacts with.

Unit Tests

Here you can see the unit tests I have created, I am leveraging a setup method to load a mock with data that could be returned from our use case. This should decouple the actual tests from the implementation and gives one place that will need to be updated in future if the return values from the use case change.

package uk.co.benashwell.cocktailrecipe.web;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.ArrayList;
import java.util.Arrays;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import uk.co.benashwell.cocktailrecipe.UseCaseFactory;
import uk.co.benashwell.cocktailrecipe.usecase.UseCase;
import uk.co.benashwell.cocktailrecipe.usecase.request.GetCocktailRequest;
import uk.co.benashwell.cocktailrecipe.usecase.response.GetCocktailResponse;


@WebMvcTest(controllers = CocktailController.class)
class CocktailControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UseCaseFactory useCaseFactory;

@Mock
private UseCase usecase;

private final String COCKTAIL_NOT_FOUND_ID = "1";
private final String MOJITO_ID = "2";
private final String OLD_FASHIONED_ID = "3";

@BeforeEach
public void setup(){
when(useCaseFactory.getUseCase("GetCocktail")).thenReturn(usecase);
when(usecase.run(new GetCocktailRequest(COCKTAIL_NOT_FOUND_ID))).thenReturn(new GetCocktailResponse());
when(usecase.run(new GetCocktailRequest(MOJITO_ID))).thenReturn(new GetCocktailResponse("Mojito",
Arrays.asList("2oz White Rum", "1oz Lime Juice", "1oz Simple Syrup", "8-12 Mint Leaves")));
when(usecase.run(new GetCocktailRequest(OLD_FASHIONED_ID))).thenReturn(new GetCocktailResponse("Old Fashioned",
Arrays.asList("2oz Bourbon", "1/4oz Rich Demerara Syrup", "3 Dashes Angostura Bitters", "2 Dashes Orange Bitters")));
}

@Test
@DisplayName("Get Cocktail API returns 200 status code")
void getCocktailById_200StatusCodeTest() throws Exception {
mockMvc.perform(get("/cocktail/" + COCKTAIL_NOT_FOUND_ID))
.andExpect(status().is(200));
}


@Test
@DisplayName("Get Cocktail API returns empty body when cocktail not found")
void getCocktailById_emptyBody_when_cocktailNotFoundTest() throws Exception {
mockMvc.perform(get("/cocktail/" + COCKTAIL_NOT_FOUND_ID))
.andExpect(content().json("{}"));
}

@Test
@DisplayName("Get Cocktail API returns mojito cocktail body")
void getCocktailById_MojitoIsFound_whenRequestedTest() throws Exception {
mockMvc.perform(get("/cocktail/" + MOJITO_ID))
.andExpect(content().json("{" +
"'name':'Mojito'," +
"'ingredients': [" +
"'2oz White Rum'," +
"'1oz Lime Juice'," +
"'1oz Simple Syrup'," +
"'8-12 Mint Leaves'" +
"]}"));
}

@Test
@DisplayName("Get Cocktail API returns Old Fashioned cocktail body")
void getCocktailById_OldFashionedIsFound_whenRequestedTest() throws Exception {
mockMvc.perform(get("/cocktail/" + OLD_FASHIONED_ID))
.andExpect(content().json("{" +
"'name': 'Old Fashioned'," +
"'ingredients': [" +
"'2oz Bourbon'," +
"'1/4oz Rich Demerara Syrup'," +
"'3 Dashes Angostura Bitters'," +
"'2 Dashes Orange Bitters'" +
"]}"));
}
}

Implementation

Below you will see the Spring controller with a GET API for /cocktail/{id} which can be used to retrieve individual Cocktails. This moves our failing acceptance test to a new issue, a null pointer exception as currently, the use case factory returns null. PROGRESS!!!

package uk.co.benashwell.cocktailrecipe.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import uk.co.benashwell.cocktailrecipe.UseCaseFactory;
import uk.co.benashwell.cocktailrecipe.usecase.UseCaseResponse;
import uk.co.benashwell.cocktailrecipe.usecase.request.GetCocktailRequest;
import uk.co.benashwell.cocktailrecipe.web.response.GetCocktailResponse;

@Controller
public class CocktailController {

@Autowired
private UseCaseFactory useCaseFactory;

@GetMapping(path = "/cocktail/{id}")
public ResponseEntity<Object> getCocktailById(@PathVariable String id) {
UseCaseResponse response = useCaseFactory.getUseCase("GetCocktail").run(new GetCocktailRequest(id));
return ResponseEntity.ok(new GetCocktailResponse(response));
}
}

To continue moving, at this moment I am just going to return a new instance of our use case. I will backfill this later when there are multiple use cases.

Closing Thoughts

Having predominantly worked with Python in my day-to-day job, I’ve been reflecting on the number of classes needed to achieve proper separation in this Spring Boot application.

On one hand, I think it makes it clear what data is needed at each layer, however, there is lots of mapping behaviour which could cause coupling in the future. I would love to hear other's opinions on this so please let me know what you think.

Next time I will be implementing the use case and moving on to the static data gateway and then potentially a deployment pipeline. In the meantime please take a look at the repository and give feedback, would love to learn from anyone who has experience building spring boot and leveraging clean architecture.

--

--