Domain
<Data>
Demand
Not an original Hexagonal concept
Just POJOs
No dependencies (but utils)
No framework-specific code
Knows only itself,
but is known to all
Domain
<Data>
Demand
Application
Does orchestration
Domain
<Service>
DemandService
<Data>
Demand
Application
Does orchestration
Knows the domain
Framework-specific
@Service, @Component, @Configuration
Domain
<Service>
DemandService
<Data>
Demand
Application
Does orchestration
Knows the domain
Framework-specific
@Service, @Component, @Configuration
Implementation of in ports
Knows nothing about Kafka Consumers, HTTP-APIs, etc.
<InPort>
ToImportDemands
Does orchestration
Knows the domain
Framework-specific
@Service, @Component, @Configuration
Implementation of in ports
Knows nothing about Kafka Consumers, HTTP-APIs, etc.
Uses out ports
Knows nothing about Postgres, Kafka Producers, etc.
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Configures/implements provided interfaces, e.g. Kafka
May use the domain,
might have its own data classes
Uses in ports
Domain
<Service>
DemandService
<Data>
Demand
<InPort>
ToImportDemands
<OutPort>
ToStoreDemands
Application
Demand
Consumer
<Data>
Demand
Message
<Data>
Demand
Message
Configures/implements outer dependencies, e.g. Postgres
May use the domain,
might have its own data classes
Implements out ports
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
Fronten Service
<Data>
Demand
Message
Application
Driving
Adapter
Driven
Adapter
Fronten Service
<Service>
Service
<InPort>
ToImport
<OutPort>
ToStore
Consumer
Store
<Data>
Entity
Repository
<Data>
Message
Controller
<Data>
Dto
<InPort>
ToQuery
KafkaProducer
<Data>
Message
<OutPort>
ToPublish
<Service>
Service
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
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
Domain
Application
Very well and easily covered by simple Unit Tests
Testing port-to-port
Requires stubbing
Test data builders
Still fast and simple unit tests
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