Hexagonal

Spring

Boot

Layered
Architecture

Presentation

Frontend
Service

Frontend
Controller

Delivery
List
Dto

Presentation

Frontend
Service

Frontend
Controller

Delivery
List
Dto

Tour
Consumer

Tour
Message

Presentation

Frontend
Service

Frontend
Controller

Logic

Delivery
List
Dto

Tour
Consumer

Tour
Message

Presentation

Frontend
Service

Frontend
Controller

Logic

Delivery
List
Dto

DeliveryList
Service

DeliveryList

Tour
Service

Tour

Tour
Consumer

Tour
Message

Presentation

Frontend
Service

Frontend
Controller

Delivery
List
Dto

DeliveryList
Service

DeliveryList

Tour
Service

Tour

Tour
Consumer

Tour
Message

Article
Service

Article

Logic

Presentation

Tour
Service

Tour

Tour
Consumer

Frontend
Service

Tour
Message

Frontend
Controller

Data

DeliveryList
Service

Article
Service

Article

DeliveryList

Delivery
List
Dto

depends on /
calls / knows

depends on /
calls / knows

Logic

Presentation

Tour
Service

Tour

Tour
Consumer

Tour
Repo.

Tour
Entity

Frontend
Service

Tour
Message

Frontend
Controller

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Delivery
List
Dto

depends on /
calls / knows

depends on /
calls / knows

Data

Logic

Presentation

Tour
Service

Tour

Tour
Consumer

Tour
Repo.

Tour
Entity

Frontend
Service

Tour
Message

Frontend
Controller

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Delivery
List
Dto

depends on /
calls / knows

depends on /
calls / knows

might "leak" into

Data

Logic

Presentation

might "leak" into

Hexagonal
Architecture

Tour
Service

Tour

Tour
Consumer

Tour
Repo.

Tour
Entity

Frontend
Service

Tour
Message

Frontend
Controller

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Delivery
List
Dto

Data

Logic

Presentation

Tour
Service

Tour

Tour
Repo.

Tour
Entity

Frontend
Service

Frontend
Controller

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Delivery
List
Dto

Tour
Consumer

Tour
Message

Data

Logic

Presentation

Tour
Service

Tour

Tour
Repo.

Tour
Entity

Frontend
Service

Frontend
Controller

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Delivery
List
Dto

Driving
Adapter

Data

Logic

Presentation

Tour
Consumer

Tour
Message

Tour
Service

Tour

Tour
Repo.

Tour
Entity

Frontend
Service

Frontend
Controller

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Delivery
List
Dto

Tour
Consumer

Tour
Message

Data

Logic

Presentation

Driving
Adapter

Tour
Service

Tour

Tour
Repo.

Tour
Entity

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Tour
Consumer

Tour
Message

Frontend
Service

Frontend
Controller

Delivery
List
Dto

Driving
Adapter

Driving
Adapter

Data

Logic

Presentation

Tour
Service

Tour

Tour
Repo.

Tour
Entity

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

Tour
Consumer

Tour
Message

Frontend
Service

Frontend
Controller

Delivery
List
Dto

DeliveryList
Service

DeliveryList

Application

Driving
Adapter

Driving
Adapter

Data

Logic

Tour
Service

Tour

Tour
Repo.

Tour
Entity

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

Tour
Consumer

Tour
Message

Frontend
Service

Frontend
Controller

Delivery
List
Dto

ToImport
Tour

DeliveryList
Service

DeliveryList

Application

Driving
Adapter

Driving
Adapter

Data

Logic

Driving
Port

knows nothing about

calls application via

Application

Driving
Adapter

Tour
Service

Tour

Tour
Repo.

Tour
Entity

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Tour
Consumer

Tour
Message

Frontend
Service

Delivery
List
Dto

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Application

Driving
Adapter

Driving
Adapter

Data

Logic

Tour
Service

Tour

Tour
Repo.

Tour
Entity

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Tour
Consumer

Tour
Message

Frontend
Service

Delivery
List
Dto

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Application

Driving
Adapter

Driving
Adapter

Data

Logic

Tour
Service

Tour

Tour
Repo.

Tour
Entity

DeliveryList
Service

Article
Service

Article

DeliveryList

Tour
Consumer

Tour
Message

Frontend
Service

Delivery
List
Dto

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Application

Driving
Adapter

Driving
Adapter

Data

ArticleData
ServiceClient

ArticleData
Dto

Driven
Adapter

Tour
Service

Tour

Tour
Repo.

Tour
Entity

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Tour
Consumer

Tour
Message

Frontend
Service

Delivery
List
Dto

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

ToGet
Article

Application

Driving
Adapter

Driving
Adapter

Data

Driven
Adapter

Driven
Port

knows nothing about

calls driven adapter via

Driving
Port

knows nothing about

calls application via

Application

Driving
Adapter

Driven
Adapter

Tour
Service

Tour

Tour
Repo.

Tour
Entity

DeliveryList
Service

Article
Service

ArticleData
ServiceClient

Article

ArticleData
Dto

DeliveryList

Tour
Consumer

Tour
Message

Driving
Adapter

Frontend
Service

Delivery
List
Dto

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Driving Port

Driven
Adapter

Application

Driven
Port

ToGet
Article

Application

Driving
Adapter

Driving
Adapter

Data

Driven
Adapter

DeliveryList

ArticleData
Dto

Tour

Tour
Service

DeliveryList
Service

Article

Article
Service

ArticleData
ServiceClient

Tour
Message

Tour
Consumer

Delivery
List
Dto

Frontend
Service

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Tour
Entity

Tour
Repo.

ToGet
Article

Application

Driving
Adapter

Driving
Adapter

Driven
Adapter

Driven
Adapter

DeliveryList

ArticleData
Dto

Tour

Tour
Service

DeliveryList
Service

Article

Article
Service

ArticleData
ServiceClient

Tour
Message

Tour
Consumer

Delivery
List
Dto

Frontend
Service

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Tour
Entity

Tour
Repo.

ToGet
Article

Application

Driving
Adapter

Driving
Adapter

Driven
Adapter

Driven
Adapter

@Repository
interface TourRepository 
  : CrudRepository<TourEntity, String>
@Service
class TourService(
  private val repository: TourRepository) {

  fun saveTour(tour: Tour) =
    repository.save(TourEntity(tour))

  // …
}
@Service
class TourStore(
  private val repository: TourRepository) : ToStoreTour {

  override fun storeTour(tour: Tour) =
    repository.save(TourEntity(tour)).toTour()
}

DeliveryList

ArticleData
Dto

Tour

Tour
Service

DeliveryList
Service

Article

Article
Service

ArticleData
ServiceClient

Tour
Message

Tour
Consumer

Delivery
List
Dto

Frontend
Service

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Tour
Entity

Tour
Repo.

ToGet
Article

Application

Driving
Adapter

Driving
Adapter

Driven
Adapter

Driven
Adapter

@Repository
interface TourRepository 
  : CrudRepository<TourEntity, String>
@Service
class TourService(
  private val tourStore: ToStoreTour) {

  fun storeTour(tour: Tour) =
    tourStore.storeTour(tour)

  // …
}
@Service
class TourStore(
  private val repository: TourRepository) : ToStoreTour {

  override fun storeTour(tour: Tour) =
    repository.save(TourEntity(tour)).toTour()
}

DeliveryList

ArticleData
Dto

Tour

Tour
Service

Tour
Entity

DeliveryList
Service

Article

Article
Service

ArticleData
ServiceClient

Tour
Message

Tour
Consumer

Delivery
List
Dto

Frontend
Service

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

Tour
Repo.

ToStore
Tour

Tour
Store

ToGet
Article

Application

Driving
Adapter

Driving
Adapter

Driven
Adapter

Driven
Adapter

DeliveryList

ArticleData
Dto

Tour

Tour
Service

Tour
Entity

DeliveryList
Service

Article

Article
Service

ArticleData
ServiceClient

Tour
Message

Tour
Consumer

Delivery
List
Dto

Frontend
Service

ToImport
Tour

ToGet
DeliveryList

Frontend
Controller

ToGet
Article

Tour
Repo.

ToStore
Tour

Tour
Store

Application

Driving
Adapter

Driving
Adapter

Driven
Adapter

Driven
Adapter

Hexagonal Unit Tests

Domain

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

<Data>
Demand

Domain

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Store

Demand
Repo.

Demand
Entity

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Store

class DemandRepositoryStub :
  CrudRepositoryStub<DemandEntity, String>(),
  DemandRepository {

  fun findByOrderId(orderId: String) =
    data.values.find {
      entity -> demand.orderId == orderId
    }
}

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Store

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Store

class DemandImporterTest {

  private val demandStore : ToStoreDemands =
    DemandStore(DemandRepositoryStub())
  
  private val demandImporter : ToImportDemands =
    DemandService(demandStore)
}
 

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Store

class DemandImporterTest {

  private val demandStore : ToStoreDemands =
    DemandStore(DemandRepositoryStub())
  
  private val demandImporter : ToImportDemands =
    DemandService(demandStore)
    
  fun `importDemand`() {
    val result = demandImporter.save(aDemand().build())
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNotNull()
  }
}
 

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Demand
Store

<InPort>
ToImportDemands

class DemandImporterTest {

  private val demandStore : ToStoreDemands =
    DemandStore(DemandRepositoryStub())
  
  private val demandImporter : ToImportDemands =
    DemandService(demandStore)
    
  fun `importDemand`() {
    val result = demandImporter.save(aDemand().build())
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNotNull()
  }
  
  fun `importDemand conflict`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand)
    
    val result = demandImporter.save(preStoredDemand)
    
    assertThat(result.isConflict()).isTrue()
  }
}
 

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Demand
Store

<InPort>
ToImportDemands

class DemandImporterTest {

  private val demandStore : ToStoreDemands =
    DemandStore(DemandRepositoryStub())
  
  private val demandImporter : ToImportDemands =
    DemandService(demandStore)
    
  fun `importDemand`() {
    val result = demandImporter.save(aDemand().build())
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNotNull()
  }
  
  fun `importDemand conflict`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand)
    
    val result = demandImporter.save(preStoredDemand)
    
    assertThat(result.isConflict()).isTrue()
  }
  
  fun `deleteDemand`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand))
    
    val result = demandImporter.delete(preStoredDemand)
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNull()
  }
}
 

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Demand
Store

<InPort>
ToImportDemands

class DemandImporterTest {

  private val demandStore : ToStoreDemands =
    DemandStore(DemandRepositoryStub())
  
  private val demandImporter : ToImportDemands =
    DemandService(demandStore)
    
  fun `importDemand`() {
    val result = demandImporter.save(aDemand().build())
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNotNull()
  }
  
  fun `importDemand conflict`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand)
    
    val result = demandImporter.save(preStoredDemand)
    
    assertThat(result.isConflict()).isTrue()
  }
  
  fun `deleteDemand`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand))
    
    val result = demandImporter.delete(preStoredDemand)
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNull()
  }
}
 

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Demand
Store

<InPort>
ToImportDemands

class DemandImporterTest {

  private val demandStore : ToStoreDemands =
    DemandStore(DemandRepositoryStub())
  
  private val demandImporter : ToImportDemands =
    DemandService(demandStore)
    
  fun `importDemand`() {
    val result = demandImporter.save(aDemand().build())
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNotNull()
  }
  
  fun `importDemand conflict`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand)
    
    val result = demandImporter.save(preStoredDemand)
    
    assertThat(result.isConflict()).isTrue()
  }
  
  fun `deleteDemand`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand))
    
    val result = demandImporter.delete(preStoredDemand)
    
    assertThat(result.isSuccessful()).isTrue()
    assertThat(demandStore.findById(preStoredDemand.id))
        .isNull()
  }
}
 
class DemandBuilder private constructor {

  private var id: String = UUID.randomUUID().toString()
  private var version: Long = 1
  private var referenceId: String = randomReferenceId()
  // …

  companion object {
    fun aDemand() = DemandBuilder()
  }
  
  fun id(id: String) = apply { this.id = id }
  fun version(version: Long) = apply { this.version = version }
  fun referenceId(referenceId: String) = apply {
    this.referenceId = referenceId }
  // …
  
  fun build() = Demand(
    id = id,
    version = version,
    referenceId = referenceId,
    // …
  )
}
 

Demand
Repo.Stub

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Demand
Store

<InPort>
ToImportDemands

Domain

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

Demand
Entity

<Data>
Demand
Message

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

Hexagonal Integration Tests

Domain

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

Domain

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

Domain

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest() : AbstractServiceTest {
}
 

<InPort>
ToImportDemands

Domain

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
  @Autowired private val demandProducer: DemandProducer,
) : AbstractServiceTest {
}
 

<InPort>
ToImportDemands

@Component
class DemandProducer(
  @Autowired kafkaTemplate: KafkaTemplate<String, String>,
  @Value("\${kafka.topics.demand.name}") topicName: String
) {

  fun send() {
    val result: CompletableFuture<SendResult<String, String>> =
      kafkaTemplate.send(topic, key, data)
    kafkaTemplate.flush()
    result.get(10, SECONDS)
  }
}

Domain

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
  @Autowired private val demandProducer: DemandProducer,
  @Autowired private val demandStore: ToStoreDemands
) : AbstractServiceTest {
}
 

<InPort>
ToImportDemands

Domain

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
  @Autowired private val demandProducer: DemandProducer,
  @Autowired private val demandStore: ToStoreDemands
) : AbstractServiceTest {

  fun `consume demand`() {
    val demandMessage = aDemandMessage().build()
    demandProducer.send(demandMessage)
    
    await().untilAsserted {
      assertThat(demandStore.findById(demandMessage.id))
        .isNotNull()
    }
  }
}
 

<InPort>
ToImportDemands

Domain

<Service>
DemandService

<Data>
Demand

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

@SpringBootTest
@EmbeddedKafka
class DemandCosumptionTest(
  @Autowired private val demandProducer: DemandProducer,
  @Autowired private val demandStore: ToStoreDemands
) : AbstractServiceTest {

  fun `consume demand`() {
    val demandMessage = aDemandMessage().build()
    demandProducer.send(demandMessage)
    
    await().untilAsserted {
      assertThat(demandStore.findById(demandMessage.id))
        .isNotNull()
    }
  }
  
  fun `consume tombstone`() {
    val preStoredDemand = aDemand().build()
    demandStore.save(preStoredDemand)
    
    demandProducer.sendTombstone(preStoredDemand.id)
    
    await().untilAsserted {
      assertThat(demandStore.findById(demandMessage.id))
        .isNull()
    }
  }
}
 

<InPort>
ToImportDemands

Domain

<Service>
DemandService

<Data>
Demand

<InPort>
ToImportDemands

<OutPort>
ToStoreDemands

Application

Demand
Consumer

<Data>
Demand
Message

Demand
Store

Demand
Repo.

Demand
Entity

<Data>
Demand
Message

Hexagonal Testing

Domain

Application

Domain

Very well and easily covered by simple Unit Tests

Application

Testing port-to-port

Requires stubbing

Test data builders

Still fast and simple unit tests

Adapters

Mostly about infrastructure

Unit Tests rather pointless

Focussed Integration Tests or Service Tests necessary

@SpringBootTest

Testcontainers

ports

application

adapters

SpringBootApplication

driven

driving

domain

driven

driving

Hexagonal Spring Boot

By Michael Kutz

Hexagonal Spring Boot

Are you tired of brittle test suites, that keep breaking for no good reasons at all? Does maintaining your test suite feels like doubling your efforts on a service? Do you avoid big refactoring because you're afraid of your own tests? Well, I've been there… In this talk I demonstrate a concept of how to achieve a great test coverage on hexagonal services written in Spring Boot with minimal test code, no mocks and great expression of intent. Never done a hexagonal service? No problem! I'll explain all core concepts in the first part of the talk to make sure we're all on the same page. After hearing this, you should have a good idea of how to refactor your tests and know some new helpful libraries and design patterns for tests.

  • 223