Implementing the Domain Services (Hexagonal Architecture with Spring Boot — Part 3)
This is the third article of a 4 5 part series explaining Hexagonal Architecture and how to implement such an architecture using Spring Boot and TDD.
Part 1 - Introduction to Hexagonal Architecture and Key Concepts
Part 2 - Coding demo project and implementation of the API adaptor
Part 3 - Implementation of Domain Services
Part 4 - Implementation of MongoDB repository adaptor
Part 5 - Implementation of REST adaptor to an external data source
Part 2 of the video is live now:
Domain Services
At the end of implementing the REST API adaptor, we have mocked out two domain services GetStockPositionService
and GetStockMarketValueService
.
As you go through the code below, notice that we will write the domain services functionally, and we can test them without using the Spring framework for dependency injection. Testing domain services functionally and without the Spring framework enables very fast testing.
Also, notice that where the domain services need to call some adaptors, whether data repository or external services, a port to interface is used. Again, in the tests, we specify the required behaviour of these adaptors through these ports, and the domain services does not have to know any further details of these adaptors beyond their behaviours.
Let’s start with the test for GetStockPositionService
, GetStockPositionServiceTest
as shown here:
package com.example.hexademo.domain.service; | |
// imports not shown | |
class GetStockPositionServiceTest { | |
private final StockPositionsRepository repository = mock(StockPositionsRepository.class); | |
private final GetStockPositionService subject = new GetStockPositionService(repository); | |
@Test | |
void get() { | |
// arrange | |
String user = DomainModelFaker.fakeUser(); | |
String symbol = DomainModelFaker.fakeStockSymbol(); | |
StockPosition fakeStockPosition = DomainModelFaker.fakeStockPosition(user, symbol); | |
when(repository.findOneByUserAndSymbol(user, symbol)).thenReturn(Mono.just(fakeStockPosition)); | |
// act | |
Mono<StockPosition> result = subject.get(user, symbol); | |
// assert | |
StepVerifier.create(result) | |
.expectNext(fakeStockPosition) | |
.verifyComplete(); | |
} | |
} |
The repository required for this domain service is specified as an interface and provided directly to the service as a constructor parameter.
package com.example.hexademo.domain.service; | |
// imports not shown | |
@Service | |
public class GetStockPositionService { | |
private final StockPositionsRepository repository; | |
public GetStockPositionService(StockPositionsRepository repository) { | |
this.repository = repository; | |
} | |
public Mono<StockPosition> get( | |
String user, | |
String symbol | |
) { | |
return repository.findOneByUserAndSymbol(user, symbol); | |
} | |
} |
The other domain service GetStockMarketValueService
and its test GetStockMarketValueServiceTest
are written in a similar style.
package com.example.hexademo.domain.service; | |
// imports not shown | |
class GetStockMarketValueServiceTest { | |
private GetStockMarketPricePort getStockMarketPricePort = mock(GetStockMarketPricePort.class); | |
private GetStockMarketValueService subject = new GetStockMarketValueService(getStockMarketPricePort); | |
@Test | |
void get() { | |
// arrange | |
String symbol = DomainModelFaker.fakeStockSymbol(); | |
BigDecimal fakeQuantity = DomainModelFaker.fakeQuantity(); | |
BigDecimal fakePrice = DomainModelFaker.fakeAmount(); | |
when(getStockMarketPricePort.get(symbol)).thenReturn(Mono.just(fakePrice)); | |
// act | |
Mono<BigDecimal> result = subject.get(symbol, fakeQuantity); | |
// assert | |
StepVerifier.create(result) | |
.expectNextMatches(amount -> amount.equals(fakeQuantity.multiply(fakePrice))) | |
.verifyComplete(); | |
} | |
} |
package com.example.hexademo.domain.service; | |
// imports not shown | |
@Service | |
public class GetStockMarketValueService { | |
private final GetStockMarketPricePort getStockMarketPricePort; | |
public GetStockMarketValueService(GetStockMarketPricePort getStockMarketPricePort) { | |
this.getStockMarketPricePort = getStockMarketPricePort; | |
} | |
public Mono<BigDecimal> get( | |
String symbol, | |
BigDecimal quantity) { | |
return getStockMarketPricePort.get(symbol).map(price -> price.multiply(quantity)); | |
} | |
} |
GetStockMarketValueService
requires a source for the current market price of the stock for calculating its result. For now, we’ll specify an port as an interface GetStockMarketPricePort
for this service.