diff --git a/README.md b/README.md index 4c593a9..6ad68bd 100644 --- a/README.md +++ b/README.md @@ -73,31 +73,56 @@ by using `ports.provided` package ("provided by domain") and expects some functi - Frameworks (less are better) - Database, network clients (no SQL Exposed calls, no Kafka calls, no SQS call etc) - No transport logic (e.g. no JSON parsing, no JSON annotations etc). It might be tempting to use JSON objects received -from web service controllers, but don't it, use **ports** for DTO (Data Transfer Objects) to communicate with +from web service controllers, but don't do this, use **ports** for DTO (Data Transfer Objects) to communicate with **adapters** module. It will require extra work and some extra effort to write data structures that might look very similar, but when you decide to change your JSON payload, and your business logic will remain the same - it will pay off. - DAO +#### Ports module + +Ports is a bridge between Adapters and Domain. Domain declare interfaces it requires from Adapters and put them +in `ports.required` package. Then Adapters will provide implementations (injected via Dependency Injection, we use +Koin framework in our application). If Domain wants to share some functionality (usually business services) with +Adapters - then it will provide interfaces in `ports.provided` for Adapters to use. Also, Ports can declare data +classes (or POJOs) to allow Domain and Adapters communicate to each other. + +###### Depends on +No module dependencies + +###### What should be in ports +- `ports.provided` - interfaces for *provided* Domain services to be called from Adapters (in our case domain services will be +called from web service routes from **adapters** module). DTO objects for Domain<=>Adapters communication can be +declared here as well. +- `ports.required` - interfaces for Adapters services *required* by Domain to perform its business logic. + +###### What should not be in ports +- Anything else + + #### Adapters module Platform-specific code. Don't put any of your business logic here. ###### Depends on -- **ports** module. Adapters use this module to fulfill Domain need. For example Domain needs an access in -getting access to persistent storage (and Domain does not care how data is stored) to add/delete/update Address Book -item. In this case Domain will add its requirements into `ports.required` package and Adapters should fulfill -these requirements. In our app Domain declares an interface `ports.requires.addressbook.AddressBookItemRepository` +- **ports** module. Adapters use this module to fulfill Domain need. For example Domain needs an access to persistent +storage (and Domain does not care how data is stored) to add/delete/update Address Book item. In this +case Domain will add its requirements into `ports.required` package and Adapters should fulfill +these requirements. In our app Domain declares an interface `ports.required.addressbook.AddressBookItemRepository` and Adapters provides a database-specific implementation of this interface in -`adapters.db.AddressBookItemRepositoryDbImpl` class. +`adapters.db.AddressBookItemRepositoryDbImpl` class. Also Domain needs to generate random AddressBook item and it +adds this requirement via `ports.required.RandomPersonClient` interface and lets Adapters model to choose what +method to choose (pick from database, generate using its own algorith etc). In our case Adapters decided to +call free 3rd party REST service to fetch for random names and contact information and injects +`adapters.clients.randomperson.RandomPersonHttpClient` via dependency injection. ###### What should be in adapters - Web Service controllers (in our app it is **ktor** handlers) - HTTP/REST clients -- Kafka code to post data +- Kafka code to send or receive data - Database repositories to provide access to underlying database. - DAO objects. Yes, DAO objects should not be in **ports** or **adapters** modules if they contain framework-specific code or annotations (e.g. JPA annotations). Exceptions can be made, for example Exposed SQL uses plain data classes -and therefore we decided to put them in **ports**, therefore we use this classes not only as DAO but as DTO objects +and we decided to put them in **ports**, therefore we use this classes not only as DAO but as DTO objects as well (something that we transfer between Adapters and Domain). In your case you might want to consider using DAO objects in Adapters only and have transformers to convert DAO objects to DTO objects (DTO objects declared in `ports.provided`). @@ -106,26 +131,6 @@ in `ports.provided`). - Avoid any business logic code -#### Ports module - -Ports is a bridge between Adapters and Domain. Domain declare interfaces it requires from Adapters and put them -in `ports.required` package. Then Adapters will provide implementations (injected via Dependency Injection, we use -Koin framework in our application). If Domain wants to share some functionality (usually business services) with -Adapters - then it will provide interfaces in `ports.provided`, implement them and use DI to inject for Adapters -to use. - -###### Depends on -No module dependencies - -###### What should be in ports -- `ports.provided` - interfaces for *provided* Domain services to be called from Adapters (in our case domain services will be -called from web service routes from **adapters** module). DTO objects for Domain<=>Adapters communication can be -declared here as well. -- `ports.required` - interfaces for Adapters services *required* by Domain to perform its business logic. - -###### What should not be in ports -- Anything else - #### App module Application launcher. Should be a very simple code. Can contain application specific resources. @@ -134,28 +139,30 @@ Application launcher. Should be a very simple code. Can contain application spec All other modules. -#### Ports module - -Ports is a bridge between Adapters and Domain. Domain declares interfaces it requires from Adapters and put them -in `ports.required` package. Then Adapters will provide implementations (injected via Dependency Injection, we use -Koin framework in our application). If Domain wants to share some functionality (usually business services) with -Adapters - then it will provide interfaces in `ports.provided`, implement them and use DI to inject for Adapters -to use. - -###### Depends on -No module dependencies - #### Shared module **shared** module should not be used too often. It may contain some utility functionality, for example in our app we -have some logger utility functions in shared module, because logging is required in both **adapter** and **domain** +have some logger utility functions in the shared module, because logging required in both **adapter** and **domain** modules. ###### Depends on No module dependencies - +### Example of workflow in modules + +1. User performs HTTP POST request to /addressBookItems to create new AddressBook item. This request handled by +REST controller in `adapters.routes.AddressBookItemRoute` class. JSON payload is deserialized and copied into +`ports.provided.addressbook.SaveAddressBookItemRequestDto` data class required by Domain's service. +2. REST controller calls method `addAddressBookItem(...)` declared by interface `ports.provided.AddressBoookService` +(and implemented by **domain** module in `AddressBookServiceImpl` class) +3. Domain code in class `domain.addressbook.AddressBookServiceImpl` performs business logic related to validating and +storing AddressBook item received as DTO object from REST controller. At some point of time domain logic requires storing +of new item in persistent storage. Domain code calls method `upsert(...)` (update or insert) declared by interface +`AddressBookItemRepository` (and implemented by Adapters code in `AddressBookItemRepositoryDbImpl` class) as well as +method `upsert(...)` declared by interface `PostalAddressRepository`. +4. Domain's service code returns result back to Adapters REST controller when it gets serialized into JSON and returned +back to user. # Setup diff --git a/adapters/src/main/kotlin/adapters/AdapterModule.kt b/adapters/src/main/kotlin/adapters/AdapterModule.kt index 8e1437a..2659c10 100644 --- a/adapters/src/main/kotlin/adapters/AdapterModule.kt +++ b/adapters/src/main/kotlin/adapters/AdapterModule.kt @@ -17,7 +17,7 @@ import adapters.http.HttpClientFactoryImpl import adapters.routes.AddressBookItemRoute import adapters.routes.HealthCheckRoute import adapters.services.healthcheck.HealthCheckService -import adapters.util.DateSupplierImpl +import adapters.util.DateSupplierSystemTimeImpl import ports.required.TransactionService import ports.required.addressbook.AddressBookItemRepository import ports.required.addressbook.PostalAddressRepository @@ -25,7 +25,7 @@ import com.zaxxer.hikari.HikariDataSource import io.ktor.application.Application import org.koin.core.scope.Scope import org.koin.dsl.module -import ports.provided.util.DateSupplier +import ports.required.util.DateSupplier import ports.required.randomperson.RandomPersonClient import javax.sql.DataSource @@ -37,7 +37,7 @@ val envModule = module(createdAtStart = true) { EnvironmentVariablesImpl() } single { - DateSupplierImpl() + DateSupplierSystemTimeImpl() } } diff --git a/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonHttpClient.kt b/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonHttpClient.kt index 4ea3ee1..c0d2149 100644 --- a/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonHttpClient.kt +++ b/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonHttpClient.kt @@ -5,7 +5,7 @@ import adapters.http.HttpClientFactory import io.ktor.client.request.get import io.ktor.client.request.parameter import mu.KotlinLogging -import ports.required.randomperson.RandomPerson +import ports.required.randomperson.RandomPersonResponseDto import ports.required.randomperson.RandomPersonClient import shared.util.d @@ -16,7 +16,7 @@ class RandomPersonHttpClient( private val httpClientFactory: HttpClientFactory ) : RandomPersonClient { - override suspend fun fetchRandomPerson(): RandomPerson { + override suspend fun fetchRandomPerson(): RandomPersonResponseDto { logger.d("fetchRandomPerson") { "Perform HTTP GET request to URL=${appConfig.randomPerson.fetchUrl}" } val response: RandomPersonResponse = httpClientFactory.httpClient() .get(urlString = appConfig.randomPerson.fetchUrl) { diff --git a/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonTransformers.kt b/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonTransformers.kt index cc9a128..b876b69 100644 --- a/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonTransformers.kt +++ b/adapters/src/main/kotlin/adapters/clients/randomperson/RandomPersonTransformers.kt @@ -1,9 +1,9 @@ package adapters.clients.randomperson -import ports.required.randomperson.RandomPerson +import ports.required.randomperson.RandomPersonResponseDto internal fun RandomPersonResponse.toRandomPerson() = with(results.first()) { - RandomPerson( + RandomPersonResponseDto( firstName = name.first, lastName = name.last, gender = gender, diff --git a/adapters/src/main/kotlin/adapters/services/healthcheck/HealthCheckService.kt b/adapters/src/main/kotlin/adapters/services/healthcheck/HealthCheckService.kt index a112904..d207a52 100644 --- a/adapters/src/main/kotlin/adapters/services/healthcheck/HealthCheckService.kt +++ b/adapters/src/main/kotlin/adapters/services/healthcheck/HealthCheckService.kt @@ -2,7 +2,7 @@ package adapters.services.healthcheck import adapters.config.AppConfig import ports.provided.healthcheck.HealthCheckResponseDto -import ports.provided.util.DateSupplier +import ports.required.util.DateSupplier import java.util.Date class HealthCheckService( diff --git a/adapters/src/main/kotlin/adapters/util/DateSupplierImpl.kt b/adapters/src/main/kotlin/adapters/util/DateSupplierSystemTimeImpl.kt similarity index 50% rename from adapters/src/main/kotlin/adapters/util/DateSupplierImpl.kt rename to adapters/src/main/kotlin/adapters/util/DateSupplierSystemTimeImpl.kt index 8839eda..2b56385 100644 --- a/adapters/src/main/kotlin/adapters/util/DateSupplierImpl.kt +++ b/adapters/src/main/kotlin/adapters/util/DateSupplierSystemTimeImpl.kt @@ -1,8 +1,8 @@ package adapters.util -import ports.provided.util.DateSupplier +import ports.required.util.DateSupplier -class DateSupplierImpl : DateSupplier { +class DateSupplierSystemTimeImpl : DateSupplier { override fun currentTimeMillis() = System.currentTimeMillis() } diff --git a/app/src/test/kotlin/app/DateSupplierTestImpl.kt b/app/src/test/kotlin/app/DateSupplierTestImpl.kt index 110f865..f1f7cc2 100644 --- a/app/src/test/kotlin/app/DateSupplierTestImpl.kt +++ b/app/src/test/kotlin/app/DateSupplierTestImpl.kt @@ -1,6 +1,6 @@ package app -import ports.provided.util.DateSupplier +import ports.required.util.DateSupplier class DateSupplierTestImpl : DateSupplier { diff --git a/app/src/test/kotlin/app/TestModule.kt b/app/src/test/kotlin/app/TestModule.kt index e19a12f..16ac819 100644 --- a/app/src/test/kotlin/app/TestModule.kt +++ b/app/src/test/kotlin/app/TestModule.kt @@ -2,7 +2,7 @@ package app import adapters.config.EnvironmentVariables import org.koin.dsl.module -import ports.provided.util.DateSupplier +import ports.required.util.DateSupplier // Environment-specific configuration val envTestModule = module(createdAtStart = true) { diff --git a/app/src/test/kotlin/app/routes/HealthCheckRouteTest.kt b/app/src/test/kotlin/app/routes/HealthCheckRouteTest.kt index 30a78c1..af600d5 100644 --- a/app/src/test/kotlin/app/routes/HealthCheckRouteTest.kt +++ b/app/src/test/kotlin/app/routes/HealthCheckRouteTest.kt @@ -12,7 +12,7 @@ import org.koin.experimental.property.inject import org.koin.ktor.ext.inject import org.spekframework.spek2.style.specification.describe import ports.provided.healthcheck.HealthCheckResponseDto -import ports.provided.util.DateSupplier +import ports.required.util.DateSupplier object HealthCheckRouteTest : AppRouteSpek({ diff --git a/domain/src/main/kotlin/domain/addressbook/RandomPersonToAddressBookEntityTransformers.kt b/domain/src/main/kotlin/domain/addressbook/RandomPersonToAddressBookEntityTransformers.kt index 5b51746..d5a0530 100644 --- a/domain/src/main/kotlin/domain/addressbook/RandomPersonToAddressBookEntityTransformers.kt +++ b/domain/src/main/kotlin/domain/addressbook/RandomPersonToAddressBookEntityTransformers.kt @@ -3,7 +3,7 @@ package domain.addressbook import ports.required.addressbook.AddressBookItem import ports.required.addressbook.Gender import ports.required.addressbook.PostalAddress -import ports.required.randomperson.RandomPerson +import ports.required.randomperson.RandomPersonResponseDto import java.lang.IllegalArgumentException fun String.toGender(): Gender { @@ -16,7 +16,7 @@ fun String.toGender(): Gender { } } -fun RandomPerson.buildAddressBookItem() = AddressBookItem( +fun RandomPersonResponseDto.buildAddressBookItem() = AddressBookItem( id = null, firstName = firstName, lastName = lastName, @@ -26,7 +26,7 @@ fun RandomPerson.buildAddressBookItem() = AddressBookItem( email = email ) -fun RandomPerson.buildPostalAddress( +fun RandomPersonResponseDto.buildPostalAddress( addressBookItemId: Long ) = PostalAddress( id = null, diff --git a/ports/src/main/kotlin/ports/required/randomperson/RandomPersonClient.kt b/ports/src/main/kotlin/ports/required/randomperson/RandomPersonClient.kt index eb59042..31596c3 100644 --- a/ports/src/main/kotlin/ports/required/randomperson/RandomPersonClient.kt +++ b/ports/src/main/kotlin/ports/required/randomperson/RandomPersonClient.kt @@ -1,5 +1,5 @@ package ports.required.randomperson interface RandomPersonClient { - suspend fun fetchRandomPerson(): RandomPerson + suspend fun fetchRandomPerson(): RandomPersonResponseDto } diff --git a/ports/src/main/kotlin/ports/required/randomperson/RandomPerson.kt b/ports/src/main/kotlin/ports/required/randomperson/RandomPersonResponseDto.kt similarity index 89% rename from ports/src/main/kotlin/ports/required/randomperson/RandomPerson.kt rename to ports/src/main/kotlin/ports/required/randomperson/RandomPersonResponseDto.kt index b25c572..9b8227d 100644 --- a/ports/src/main/kotlin/ports/required/randomperson/RandomPerson.kt +++ b/ports/src/main/kotlin/ports/required/randomperson/RandomPersonResponseDto.kt @@ -1,6 +1,6 @@ package ports.required.randomperson -data class RandomPerson( +data class RandomPersonResponseDto( val firstName: String, val lastName: String, val gender: String, diff --git a/ports/src/main/kotlin/ports/provided/util/DateSupplier.kt b/ports/src/main/kotlin/ports/required/util/DateSupplier.kt similarity index 94% rename from ports/src/main/kotlin/ports/provided/util/DateSupplier.kt rename to ports/src/main/kotlin/ports/required/util/DateSupplier.kt index 6627b28..ca17783 100644 --- a/ports/src/main/kotlin/ports/provided/util/DateSupplier.kt +++ b/ports/src/main/kotlin/ports/required/util/DateSupplier.kt @@ -1,4 +1,4 @@ -package ports.provided.util +package ports.required.util // While some functions look very obvious, this class is still useful when we want to perform a unit test // Just an example, HealthRoute might return a current timestamp and we want to validate that it is current