Skip to content

Lab 9: Domain Repository

Description

In this lab you'll expand the domain objects used for coffee orders, retrieve an order from a collection-like Repository, and incorporate these into the API controller.

Goals

  • Learn how to extract information from a URI path
  • Learn how to add non-component objects to the ApplicationContext
  • Reinforce injecting dependencies to components

a. New CoffeeOrder Aggregate

In this step, we'll introduce the files needed to expand our domain objects to include the "owning" object (or aggregate): the CoffeeOrder.

The coffee order can contain multiple coffee items.

  1. Copy the CoffeeOrder.java class to the domain package in the src directory.

  2. Copy the CoffeeOrderResponse.java class to the adapter.in.api package in the src directory.

  3. Copy the CoffeeOrderWebTest.java to overwrite the existing one in the test directory.

  4. In the CoffeeOrderController, replace the existing method with the following:

    @GetMapping("/api/coffee/orders/{id}")
    public CoffeeOrderResponse coffeeOrder(@PathVariable("id") long orderId) {
      CoffeeItem coffeeItem = new CoffeeItem("small", "latte", "milk");
      coffeeItem.setId(99L);
      CoffeeOrder coffeeOrder = new CoffeeOrder("Ted", LocalDateTime.of(2020, 10, 11, 12, 13));
      coffeeOrder.add(coffeeItem);
      coffeeOrder.setId(orderId);
      return CoffeeOrderResponse.from(coffeeOrder);
    }
    

    URI Path Template Variables

    For more information on how template variables work, such as @PathVariable and {id} used above, see: https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-ann-requestmapping-uri-templates

  5. Run the CoffeeOrderWebTest, which should pass.

  6. Run the application and access it via the browser: http://localhost:8080/api/coffee/orders/42

    Browsers Prefer XML

    Because browsers "prefer" XML in terms of what they accept (as they don't have JSON in their Accept header), you will see XML returned instead of JSON. That's totally fine! If you'd like, you can use curl or a tool like Insomnia or Postman to force the response to be JSON by setting the Accept: header to be application/json, e.g.:

    curl -v -H "Accept: application/json" "http://localhost:8080/api/coffee/orders/23"
    

b. Copy Repository Files

Now we need a place to store (and retrieve) those Coffee Orders. For this, we'll use the Domain Repository Pattern.

  1. Copy the CoffeeOrderRepository.java interface file into your code directory in the domain package.

  2. Copy the InMemoryCoffeeOrderRepository.java implementation also into the domain package.

c. Configure In-Memory Repository

Since the InMemoryCoffeeOrderRepository is not a Spring-managed bean, but a Domain Object, we need to provide a bridge so that Spring can become aware of it, without coupling the Domain to the Spring framework.

To do this, we'll use Spring's @Configuration and @Bean annotations.

  1. Create a new src class CoffeeOrderRepositoryConfig in the com.welltestedlearning.coffeekiosk package.

  2. Add the @Configuration annotation to the class, so that Spring will find it during component scanning.

  3. Add the following method:

    @Bean
    public CoffeeOrderRepository inMemoryCoffeeOrderRepository() {
      return new InMemoryCoffeeOrderRepository();
    }
    

    This will tell Spring to add the InMemoryCoffeeOrderRepository instance to its ApplicationContext so that it can be auto-wired wherever needed.

  4. To test that you've configured things correctly so far, open up the CoffeeKioskApplicationTests test and add:

    @Autowired
    CoffeeOrderRepository coffeeOrderRepository;
    
  5. Run the contextLoads test (in the CoffeeKioskApplicationTests class), which will cause Spring to attempt to auto-wire the repository.

    If things are configured correctly, the test will pass.

    If not, then you'll get an error message like:

    org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type
    'com.welltestedlearning.coffeekiosk.domain.CoffeeOrderRepository' available:
    
  6. Add the following test (to CoffeeKioskApplicationTests):

    @Test
    public void sampleDataWasLoaded() throws Exception {
        assertThat(coffeeOrderRepository.findAll())
                .hasSize(1);
    }
    

    and run it. It will fail because the repository is empty. We'll fix that in the next step.

d. Pre-load Repository

So that we'll always have some sample data loaded into the repository, we'll create a startup loader similar to what we did in Lab 3.

  1. Create a new src class, SampleDataLoader in the root package (com.welltestedlearning.coffeekiosk).

  2. Annotate it as a component, and implement the appropriate interface so that it gets run upon startup.

  3. Add a private final instance field for the CoffeeOrderRepository and have it auto-wired via constructor injection.

  4. In the run() method, instantiate a CoffeeItem and CoffeeOrder like you did in Step A above, then call the repository's save() method to save it.

  5. Run the CoffeeKioskApplicationTests test and if everything was done correctly, the tests should pass.

  6. Run all of the tests, which should also now be passing.

e. GET By Order ID

Now that the repository is available and has data loaded, we can return that sample Coffee Order in the GET-mapped method in the controller.

  1. Open CoffeeOrderController and add a private final instance field for the CoffeeOrderRepository and have it auto-wired via constructor injection.

  2. In the GET-mapped coffeeOrder method, replace the hard-coded creation of the coffee item & order with a lookup from the repository, e.g.:

    CoffeeOrder coffeeOrder = coffeeOrderRepository.findById(orderId).get();
    

    The .get() is required as findById returns an Optional. For now, we'll ignore the potential for a null.

  3. Run the application and using your browser or curl, etc., go to: http://localhost:8080/api/coffee/orders/23

What happens if you use a number other than 23 here?

f. Test Configuration

If you try to run the "sliced" CoffeeOrderWebTest, you'll see that it fails to start.

This is because the controller now has an auto-wired dependency on the repository that isn't initialized for tests annotated with only @WebMvcTest.

There are several options. Try each one and run the tests to see how it works.

  1. Load the configuration and sample data classes during the test. This is straightforward, but can get more complex as you have more dependencies on various configuration files. To do this, add the following annotation to the test above the class name:

    @Import({CoffeeOrderRepositoryConfig.class, SampleDataLoader.class})
    
  2. Change to the more comprehensive -- but slower -- @SpringBootTest annotation, instead of @WebMvcTest. Remove the class annotations and add these:

    @SpringBootTest
    @AutoconfigureMockMvc
    
  3. Create a Mockito mock for the repository, which is a programmable stub, using the @MockBean annotation. This is often used, but can lead to other issues of over-mocking.

    For this example, return to just having the @WebMvcTest(CoffeeOrderController.class) as the annotation. Then add the following code to the class:

    @MockBean
    CoffeeOrderRepository coffeeOrderRepository;
    
    @BeforeEach
    public void initRepo() {
      CoffeeItem coffeeItem = new CoffeeItem("small", "latte", "milk");
      coffeeItem.setId(99L);
      CoffeeOrder coffeeOrder = new CoffeeOrder("Ted", LocalDateTime.of(2020, 10, 11, 12, 13));
      coffeeOrder.add(coffeeItem);
      coffeeOrder.setId(23L);
      org.mockito.Mockito.when(coffeeOrderRepository.findById(23L)).thenReturn(Optional.of(coffeeOrder));
    }
    

Aggregate Design Reference

See the series on Aggregate Design by Vernon Vaughn: https://kalele.io/effective-aggregate-design/