├── .gitignore ├── README.md ├── build.gradle ├── docs ├── clean_architecture │ ├── clean_architecture.drawio │ └── clean_architecture.png ├── domainstorytelling │ ├── Daily Family Tradegy.dst │ └── Daily Family Tradegy.png ├── eventstorming │ ├── DDD Setup - DDD Stickies Template.jpg │ ├── DDD Setup - DDD Stickies Template_small.jpg │ ├── DDD Setup - Event Storming - Todo Management.jpg │ └── eventsorming.md ├── onion_architecture │ ├── onion_architure.drawio │ └── onion_architure.png └── uml │ ├── domain-objects.png │ ├── uml.md │ └── usecases.png ├── gradle ├── test-acceptance.gradle ├── test-architecture.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ ├── common │ │ ├── architecture │ │ │ ├── Adapter.java │ │ │ ├── Command.java │ │ │ ├── Port.java │ │ │ └── UseCase.java │ │ └── eventbus │ │ │ ├── EventPublisher.java │ │ │ ├── EventReceiver.java │ │ │ └── impl │ │ │ ├── AbstractEventReceiverImpl.java │ │ │ └── EventPublisherImpl.java │ └── todo │ │ ├── Application.java │ │ ├── adapter │ │ ├── in │ │ │ ├── events │ │ │ │ ├── ReceiveTodoAddedEventGuavaAdapter.java │ │ │ │ ├── ReceiveTodoDoneEventGuavaAdapter.java │ │ │ │ └── ReceiveTodoListCreatedEventGuavaAdapter.java │ │ │ └── rest │ │ │ │ ├── TodoRestController.java │ │ │ │ └── model │ │ │ │ ├── AddTodoRequest.java │ │ │ │ ├── AddTodoResponse.java │ │ │ │ ├── CreateTodolistRequest.java │ │ │ │ ├── GetAllUndoneTodosResponse.java │ │ │ │ └── MappingUtil.java │ │ └── out │ │ │ ├── db │ │ │ └── TodoListListInMemoryRepository.java │ │ │ └── events │ │ │ ├── SendTodoAddedEventGuavaAdapter.java │ │ │ ├── SendTodoDoneEventGuavaAdapter.java │ │ │ └── SendTodoListCreatedEventGuavaAdapter.java │ │ ├── application │ │ ├── service │ │ │ ├── AddTodoImpl.java │ │ │ ├── CreateTodoListImpl.java │ │ │ ├── GetTodoDoneImpl.java │ │ │ └── ReadingTodosImpl.java │ │ └── usecase │ │ │ ├── AddTodo.java │ │ │ ├── CreateTodoList.java │ │ │ ├── GetTodoDone.java │ │ │ └── ReadingTodos.java │ │ ├── config │ │ ├── ApplicationConfig.java │ │ └── InfrastructureConfig.java │ │ └── domain │ │ ├── command │ │ ├── AddTodoCommand.java │ │ ├── CreateTodoListCommand.java │ │ ├── GetTodoDoneCommand.java │ │ └── ReadTodosCommand.java │ │ ├── event │ │ ├── TodoAddedEvent.java │ │ ├── TodoDoneEvent.java │ │ └── TodoListCreatedEvent.java │ │ ├── exception │ │ ├── MaxNumberOfTodosExceedException.java │ │ ├── TodoListAlreadyExistsException.java │ │ └── UserDoesNotExistException.java │ │ ├── model │ │ ├── Todo.java │ │ ├── TodoId.java │ │ ├── TodoList.java │ │ └── UserId.java │ │ └── port │ │ ├── in │ │ ├── ReceiveTodoAddedEventPort.java │ │ ├── ReceiveTodoDoneEventPort.java │ │ └── ReceiveTodoListCreatedEventPort.java │ │ └── out │ │ ├── LoadTodoListPort.java │ │ ├── SaveTodoListPort.java │ │ ├── SendTodoAddedEventPort.java │ │ ├── SendTodoDoneEventPort.java │ │ └── SendTodoListCreatedEventPort.java └── resources │ └── application.yml ├── test-acceptance ├── java │ └── todo │ │ ├── ToDoStepDefs.java │ │ └── TodoUseCaseAcceptanceTest.java └── resources │ └── todo │ ├── adding_todos.feature │ ├── getting_todo_done.feature │ └── reading_todos.feature ├── test-architecture └── java │ ├── archtest │ ├── OnionArchitecture.java │ └── PackageType.java │ └── todo │ ├── DddApplicationTest.java │ ├── DddArchitectureTest.java │ ├── DddDomainTest.java │ └── DddInfrastructureTest.java └── test └── java └── todo └── infrastructure └── adapter └── db └── TodoListInMemoryRepositoryTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .gradle/ 3 | .history/ 4 | .idea/ 5 | .project 6 | .settings/ 7 | bin/ 8 | build/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Daily Family Tradegy - a Hands on Domain Driven Design & Clean Architecture 2 | 3 | There are many different styles and interpretations of _Domain Driven Design_ (aka DDD) and the _Clean Architecture_ out there. 4 | This example should be a proof of concept how these to concepts can fit together and how _DDD_ can lead us to a clean architecture that focuses on the business (domain). 5 | The intention here is not to find the best overall solution for anybody, but to find a good and clear way for me. 6 | 7 | 8 | 9 | ## Strategic Design 10 | The main idea of _Domain Driven Design_ is about understading the appropriate domain. Therefore we'll bringt the domain experts and the developer together to explain the required know-how of a domain to the people that will build the software. 11 | A good way to do this is a method called _Domain Storytelling_. The result will give us a good overview about the actors, the activities and work objects. 12 | The separation of the actors and processes can give us a good indicator to find a _Bounded Context_. 13 | One of the most important things is the _Ubiquitous Language_. This allows us to find the same words in the software that the domain exports uses when telling about their domain 14 | 15 | 16 | ### Domain Storytelling 17 | The domain experts - the storrytellers - are telling a story about their domain works to the developers. This story we'll be drawn by a moderator. At the end everybody we'll be able to tell this story and so about the domain workflows by viewing on the drawing. 18 | 19 | 20 | #### The Scenario 21 | A wife asks her husband to fix the clogged drain. The husband is a little absent-minded and forgets about it. After a while the wife asks her husband whether to task is done and the husband has to confess that he has forgotton the task. 22 | That makes her angry and she gives her husband a complaint. 23 | So the husband takes a heart and adds the todo to a todo list. 24 | As soon he has a little spare time he wants to get an overview about his undone todos. 25 | When the todo is done, he get this todo done. 26 | Finally, the wife and her husband are both very happy and still married... 27 | 28 | ![Domain Storytelling](docs/domainstorytelling/Daily%20Family%20Tradegy.png) 29 | 30 | 31 | ### Bounded Contexts 32 | It seems that one good Bounded Context could be the `Todo Management`. It should support the husband to getting his todos done. 33 | 34 | Because of this example seems to be very simple, we are not having any other _Subdomains_ or _Bounded Contexts_ here. Otherwise the next step would be making up a _Context Map_. 35 | 36 | 37 | ### Use Cases 38 | Let's have a closer look at the operations that the husband performs to the _Bounded Context_ - these are the use cases. 39 | 40 | ![Use Cases](docs/uml/usecases.png) 41 | 42 | 43 | ### Event Storming 44 | After we identified the Bounded Context. The next step is to get more into that context. Therefore we choosed a method called `Event Storming`. 45 | 46 | ![Event Storming Legend](docs/eventstorming/DDD%20Setup%20-%20DDD%20Stickies%20Template_small.jpg) 47 | 48 | ![Event Storming Todo Management](docs/eventstorming/DDD%20Setup%20-%20Event%20Storming%20-%20Todo%20Management.jpg) 49 | 50 | Now it is more clear, what each command needs as input data and which domain events occur. 51 | 52 | 53 | ### Domain Objects 54 | The domain is the heart of our application and contains the entities, aggregates and value objects. In our example we had already identified the following objects: 55 | 56 | * User (known as 'husband') 57 | * Todo 58 | * Todo List 59 | 60 | 61 | 62 | ## Tactical Design 63 | In The tatical design we're defining what the types the domain objects are like. This can be e.g. _Aggregates_, _Entities_, _Value Objects_, _Factories_, _Services_ or _Repositories_. 64 | 65 | The identified domain objects leads us to the following structure: 66 | 67 | ![Domain Objects](docs/uml/domain-objects.png) 68 | 69 | ### Clean Architecture 70 | We want to follow the _Clean Architecture_. There are many different diagrams out there, but for me the following one makes it more clear for me. 71 | 72 | ![Clean Architecture](docs/clean_architecture/clean_architecture.png) 73 | 74 | ### Domain Layer 75 | First, we're implementing the domain objects located in `domain` layer. After that we're implementing the _Domain Services_ that have access to the _Domain Repositories_, etc. These services itselves are stateless and have access to our domain objects and can let them change their states. They are also located in the `domain` layer. There are also `Ports` (interfaces) that allows us to interact with the environment, like database or an external event bus (see infrastructure layer). 76 | 77 | ### Application Layer 78 | In the `application` layer there are only two different types. The first one are interfaces and are called `Use Cases`. It describes how an actor would interact with our domain software. The second are `Application Services` that are implementing the Use Cases and orchestrating the domain logic. 79 | 80 | 81 | Until here there is no need are any framework or third party libary. Keep your `domain` and your `application` layer clean! 82 | 83 | 84 | ### Infrastructure Layer 85 | In the `infrastructure` layer we can now add the (microservice-)framework, like _Spring Boot_, to get our business application run. Furthermore, we can here implement the required ports from the `domain` layer in classes called `Adapters`. 86 | 87 | If do so, our application is very robust when we're updating dependencies or switching technologies, like `database` integration. Then we're just having to rewrite a new adapter and that's it! 88 | 89 | ## Testing 90 | Let's see, if our tests runs: `./gradlew clean cJ test accT` 91 | 92 | ### Testing the Use Cases (using Cucumber) 93 | We are finding the acceptance tests based on _Gherkin & Cucumber_ under the path `src/test-acceptance`. The purpose of these tests is to make sure that the use cases that we implemented in the `application` layer working fine. 94 | 95 | To run the acceptance tests there is a gradle task under `gradle/test-acceptance.gradle`. 96 | 97 | ### Testing the Architecture (using ArchUnit) 98 | We are finding the architecture tests based on _ArchUnit_ under the path `src/test-architecture`. The purpose of these tests is to make sure that our source code follows the rules and structure given by _DDD_ and the _Onion Architecture_. 99 | 100 | To run the architecture tests there is a gradle task under `gradle/test-architecture.gradle`. These tests will always be executed after the `test` task runs. 101 | 102 | ## Tooling & Further Links 103 | 104 | | Purpose | Tool | Link | 105 | | --- | --- | --- | 106 | | Domain Storrytelling | WPS Domain Storrytelling Modeler | https://www.wps.de/modeler/ | 107 | | Creating Diagrams | Draw.io | https://app.diagrams.net/ | 108 | | Event Storming | Miro | https://miro.com/ | 109 | | UML Modelling | PlantUML | https://plantuml.com/ | 110 | 111 | Links: 112 | | Topic | Link | 113 | | --- | --- | 114 | | Domain Storrytelling Book | https://leanpub.com/domainstorytelling | 115 | | DDD Example | https://leasingninja.github.io/ | 116 | | Clean Architecture | https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html | 117 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.1.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.9.RELEASE' 4 | id 'org.asciidoctor.convert' version '1.5.8' 5 | id 'java' 6 | id 'idea' 7 | } 8 | 9 | apply from: 'gradle/test-architecture.gradle' 10 | apply from: 'gradle/test-acceptance.gradle' 11 | 12 | group 'ddd.example.todo' 13 | version '1.0.0-SNAPSHOT' 14 | 15 | allprojects { 16 | repositories { 17 | mavenCentral() 18 | } 19 | } 20 | 21 | ext { 22 | set('snippetsDir', file("build/generated-snippets")) 23 | } 24 | 25 | dependencies { 26 | def dddBitsVersion = '0.0.1' 27 | def lombokVersion = '1.18.12' 28 | 29 | implementation "io.hschwentner.dddbits:dddbits:${dddBitsVersion}" 30 | implementation 'org.springframework.boot:spring-boot-starter-web' 31 | 32 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 33 | implementation "com.google.guava:guava:29.0-jre" 34 | implementation "javax.annotation:javax.annotation-api:1.3.2" 35 | compileOnly "org.projectlombok:lombok:${lombokVersion}" 36 | annotationProcessor "org.projectlombok:lombok:${lombokVersion}" 37 | 38 | // test 39 | testCompileOnly "org.projectlombok:lombok:${lombokVersion}" 40 | testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" 41 | 42 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 43 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 44 | } 45 | testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' 46 | 47 | // acceptanceTest 48 | def cucumberVersion = '5.6.0' 49 | acceptanceTestImplementation "io.cucumber:cucumber-java:${cucumberVersion}" 50 | acceptanceTestImplementation "io.cucumber:cucumber-junit:${cucumberVersion}" 51 | acceptanceTestImplementation "com.google.guava:guava:29.0-jre" 52 | 53 | // architectureTest 54 | architectureTestImplementation "io.hschwentner.dddbits:dddbits:${dddBitsVersion}" 55 | architectureTestImplementation 'com.tngtech.archunit:archunit-junit4:0.13.1' 56 | architectureTestImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.6.1' 57 | } 58 | 59 | test { 60 | outputs.dir snippetsDir 61 | useJUnitPlatform() 62 | } 63 | 64 | asciidoctor { 65 | inputs.dir snippetsDir 66 | dependsOn test 67 | } 68 | -------------------------------------------------------------------------------- /docs/clean_architecture/clean_architecture.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /docs/clean_architecture/clean_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/clean_architecture/clean_architecture.png -------------------------------------------------------------------------------- /docs/domainstorytelling/Daily Family Tradegy.dst: -------------------------------------------------------------------------------- 1 | {"domain":"{\"name\":\"default\",\"actors\":{\"Person\":\"\",\"Group\":\"\",\"System\":\"\"},\"workObjects\":{\"Document\":\"\",\"Folder\":\"\",\"Call\":\"\",\"Email\":\"\",\"Conversation\":\"\",\"Info\":\"\",\"View-List\":\"\"}}","dst":"[{\"type\":\"domainStory:workObjectDocument\",\"name\":\"todo\",\"id\":\"shape_8664\",\"pickedColor\":\"black\",\"x\":1022,\"y\":382},{\"type\":\"domainStory:workObjectDocument\",\"name\":\"todo\",\"id\":\"shape_2088\",\"pickedColor\":\"black\",\"x\":1022,\"y\":242},{\"type\":\"domainStory:workObjectView-List\",\"name\":\"todo list\",\"id\":\"shape_7691\",\"pickedColor\":\"black\",\"x\":1202,\"y\":242},{\"type\":\"domainStory:workObjectDocument\",\"name\":\"todo\",\"id\":\"shape_7992\",\"pickedColor\":\"black\",\"x\":1022,\"y\":112},{\"type\":\"domainStory:textAnnotation\",\"name\":\"\",\"id\":\"shape_9641\",\"x\":790,\"y\":154,\"text\":\"is a little absent-minded and forgets tasks\",\"number\":72,\"width\":100,\"height\":72},{\"type\":\"domainStory:connection\",\"name\":\"\",\"id\":\"connection_6206\",\"waypoints\":[{\"original\":{\"x\":810,\"y\":280},\"x\":821,\"y\":248},{\"original\":{\"x\":840,\"y\":190},\"x\":828,\"y\":226}],\"source\":\"shape_7690\",\"target\":\"shape_9641\"},{\"type\":\"domainStory:actorPerson\",\"name\":\"wife\",\"id\":\"shape_3534\",\"pickedColor\":\"black\",\"x\":272,\"y\":242},{\"type\":\"domainStory:actorPerson\",\"name\":\"husband\",\"id\":\"shape_7690\",\"pickedColor\":\"black\",\"x\":772,\"y\":242},{\"type\":\"domainStory:workObjectConversation\",\"name\":\"tasks to be done\",\"id\":\"shape_3745\",\"pickedColor\":\"black\",\"x\":532,\"y\":112},{\"type\":\"domainStory:workObjectConversation\",\"name\":\"status of placed tasks\",\"id\":\"shape_8491\",\"pickedColor\":\"black\",\"x\":532,\"y\":242},{\"type\":\"domainStory:workObjectConversation\",\"name\":\"question about the tasks\",\"id\":\"shape_1041\",\"pickedColor\":\"black\",\"x\":532,\"y\":382},{\"type\":\"domainStory:workObjectConversation\",\"name\":\"complaint\",\"id\":\"shape_5822\",\"pickedColor\":\"black\",\"x\":533,\"y\":503},{\"type\":\"domainStory:activity\",\"name\":\"tells about\",\"id\":\"connection_7099\",\"pickedColor\":\"black\",\"number\":\"1\",\"waypoints\":[{\"original\":{\"x\":310,\"y\":280},\"x\":337,\"y\":264},{\"original\":{\"x\":530,\"y\":150},\"x\":530,\"y\":150}],\"source\":\"shape_3534\",\"target\":\"shape_3745\",\"multipleNumberAllowed\":false},{\"type\":\"domainStory:activity\",\"name\":\"\",\"id\":\"connection_2993\",\"pickedColor\":\"black\",\"number\":null,\"waypoints\":[{\"original\":{\"x\":570,\"y\":150},\"x\":603,\"y\":168},{\"original\":{\"x\":792,\"y\":270},\"x\":772,\"y\":259}],\"source\":\"shape_3745\",\"target\":\"shape_7690\"},{\"type\":\"domainStory:activity\",\"name\":\"\",\"id\":\"connection_1534\",\"pickedColor\":\"black\",\"number\":3,\"waypoints\":[{\"original\":{\"x\":810,\"y\":280},\"x\":772,\"y\":304},{\"original\":{\"x\":587,\"y\":419},\"x\":602,\"y\":410}],\"source\":\"shape_7690\",\"target\":\"shape_1041\"},{\"type\":\"domainStory:activity\",\"name\":\"makes angry\",\"id\":\"connection_4229\",\"pickedColor\":\"black\",\"number\":null,\"waypoints\":[{\"original\":{\"x\":570,\"y\":420},\"x\":532,\"y\":400},{\"original\":{\"x\":340,\"y\":300},\"x\":341,\"y\":301}],\"source\":\"shape_1041\",\"target\":\"shape_3534\"},{\"type\":\"domainStory:activity\",\"name\":\"asks for\",\"id\":\"connection_1886\",\"pickedColor\":\"black\",\"number\":\"2\",\"waypoints\":[{\"original\":{\"x\":310,\"y\":280},\"x\":352,\"y\":280},{\"original\":{\"x\":559,\"y\":279},\"x\":532,\"y\":279}],\"source\":\"shape_3534\",\"target\":\"shape_8491\",\"multipleNumberAllowed\":false},{\"type\":\"domainStory:activity\",\"name\":\"\",\"id\":\"connection_1002\",\"pickedColor\":\"black\",\"number\":null,\"waypoints\":[{\"original\":{\"x\":570,\"y\":280},\"x\":613,\"y\":280},{\"original\":{\"x\":792,\"y\":280},\"x\":772,\"y\":280}],\"source\":\"shape_8491\",\"target\":\"shape_7690\"},{\"type\":\"domainStory:activity\",\"name\":\"gives\",\"id\":\"connection_5394\",\"pickedColor\":\"black\",\"number\":\"4\",\"waypoints\":[{\"original\":{\"x\":310,\"y\":280},\"x\":335,\"y\":311},{\"original\":{\"x\":553,\"y\":530},\"x\":533,\"y\":509}],\"source\":\"shape_3534\",\"target\":\"shape_5822\",\"multipleNumberAllowed\":false},{\"type\":\"domainStory:activity\",\"name\":\"\",\"id\":\"connection_5624\",\"pickedColor\":\"black\",\"number\":null,\"waypoints\":[{\"original\":{\"x\":571,\"y\":541},\"x\":592,\"y\":520},{\"original\":{\"x\":810,\"y\":297},\"x\":781,\"y\":327}],\"source\":\"shape_5822\",\"target\":\"shape_7690\"},{\"type\":\"domainStory:activity\",\"name\":\"add\",\"id\":\"connection_7632\",\"pickedColor\":\"black\",\"number\":\"5\",\"waypoints\":[{\"original\":{\"x\":810,\"y\":280},\"x\":838,\"y\":265},{\"original\":{\"x\":1050,\"y\":150},\"x\":1022,\"y\":165}],\"source\":\"shape_7690\",\"target\":\"shape_7992\",\"multipleNumberAllowed\":false},{\"type\":\"domainStory:activity\",\"name\":\"\",\"id\":\"connection_6229\",\"pickedColor\":\"black\",\"number\":null,\"waypoints\":[{\"original\":{\"x\":1060,\"y\":150},\"x\":1090,\"y\":172},{\"original\":{\"x\":1239,\"y\":279},\"x\":1202,\"y\":252}],\"source\":\"shape_7992\",\"target\":\"shape_7691\"},{\"type\":\"domainStory:activity\",\"name\":\"get done\",\"id\":\"connection_1053\",\"pickedColor\":\"black\",\"number\":7,\"waypoints\":[{\"original\":{\"x\":810,\"y\":280},\"x\":842,\"y\":299},{\"original\":{\"x\":1029,\"y\":409},\"x\":1022,\"y\":405}],\"source\":\"shape_7690\",\"target\":\"shape_8664\",\"multipleNumberAllowed\":false},{\"type\":\"domainStory:activity\",\"name\":\"\",\"id\":\"connection_2146\",\"pickedColor\":\"black\",\"number\":null,\"waypoints\":[{\"original\":{\"x\":1060,\"y\":420},\"x\":1083,\"y\":401},{\"original\":{\"x\":1222,\"y\":289},\"x\":1202,\"y\":305}],\"source\":\"shape_8664\",\"target\":\"shape_7691\"},{\"type\":\"domainStory:activity\",\"name\":\"reads all undone\",\"id\":\"connection_2285\",\"pickedColor\":\"black\",\"number\":\"6\",\"waypoints\":[{\"original\":{\"x\":810,\"y\":280},\"x\":853,\"y\":280},{\"original\":{\"x\":1049,\"y\":280},\"x\":1022,\"y\":280}],\"source\":\"shape_7690\",\"target\":\"shape_2088\",\"multipleNumberAllowed\":false},{\"type\":\"domainStory:activity\",\"name\":\"\",\"id\":\"connection_3355\",\"pickedColor\":\"black\",\"number\":null,\"waypoints\":[{\"original\":{\"x\":1060,\"y\":280},\"x\":1102,\"y\":280},{\"original\":{\"x\":1229,\"y\":279},\"x\":1202,\"y\":279}],\"source\":\"shape_2088\",\"target\":\"shape_7691\"},{\"type\":\"domainStory:group\",\"name\":\"Todo Management\",\"id\":\"shape_0824\",\"pickedColor\":\"black\",\"x\":878,\"y\":90,\"height\":387,\"width\":422},{\"info\":\"The wife asks her husband to XXXX. The husband is a little absent-minded and forgets it. After a while the wife asks her husband whether to task is done and the husband has to tell her that he has forgotton the task. That makes her angry and she gives her husband a complaint. So the husband takes a heart and adds the todo to a todo list. As soon he has a little spare time he wants to get an overview about his undone todos. When the todo is done, he get this todo done.\"},{\"version\":\"1.1.1\"}]"} -------------------------------------------------------------------------------- /docs/domainstorytelling/Daily Family Tradegy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/domainstorytelling/Daily Family Tradegy.png -------------------------------------------------------------------------------- /docs/eventstorming/DDD Setup - DDD Stickies Template.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/eventstorming/DDD Setup - DDD Stickies Template.jpg -------------------------------------------------------------------------------- /docs/eventstorming/DDD Setup - DDD Stickies Template_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/eventstorming/DDD Setup - DDD Stickies Template_small.jpg -------------------------------------------------------------------------------- /docs/eventstorming/DDD Setup - Event Storming - Todo Management.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/eventstorming/DDD Setup - Event Storming - Todo Management.jpg -------------------------------------------------------------------------------- /docs/eventstorming/eventsorming.md: -------------------------------------------------------------------------------- 1 | # Event Stroming 2 | 3 | For documentation purposes, I've used Miro. 4 | 5 | [Link to the Miro board](https://miro.com/app/board/o9J_kvd8zUg=/): 6 | 7 | -------------------------------------------------------------------------------- /docs/onion_architecture/onion_architure.drawio: -------------------------------------------------------------------------------- 1 | 7Vtdd6M2EP01flwfEJ9+TOxku+1um9N0T7uPCii2GoyokGO7v74CxJeEY2wj29ntS441CAnuvTOaEcrImi43HylMFl9IiKIRMMLNyJqNALAnNv+bGbaFAdigMMwpDguTWRse8b9IGA1hXeEQpa2OjJCI4aRtDEgco4C1bJBSsm53eyZRe9YEzpFieAxgpFr/xCFbFFYfeLX9J4Tni3Jm050UV5aw7CzeJF3AkKwbJutuZE0pIaz4tdxMUZRhV+JS3He/42r1YBTFrM8NK/zHx63vvH59+eXnuyWLbr/8/umDGOUVRivxwuJh2bZEgI/CweaN2/UCM/SYwCC7suZ0c9uCLSPeMvlPmCYFA894g/ikt2JsRBna7Hxos4KCSwiRJWJ0y7uIG4Aj0BPyMW3RXtdkWIawLRpEVEYoBDCvxq4x4j8ETAdA5iqQ3SRJhAPIMIkV9LK359eiz/AJRQ8kxXkva7bEYZj1uS073ER43rrQQJZLJ8nGW27mmZONn2CKgzGkQXaNQcpu4nk+oTH2HG5DcVhbQGaJpOkDjj+iGWli2sowAGkV9iVpE5U00EUa0Eaa10PnHLQsYPBWTGKJgaI3CpVgsReTxjs7Ha9c2iiKuIBe28N3wSBmeCCYT7wTcjCRkEzJigZI3NWMEtJAzr6BuNzmiCkD5axUr308Uc6VByTLlfDpCEh+B8++LmX7B4Uj/uJMii2Mkhc0JRGhtfKfcRRJJiVQ7AxcXaxQsorDjIOZMRAPZWJR8mCoPNidIUYTDxOFh0/xM4Uc3lXAVhSNgBtl0D9R/mvOchRcuMzAKf6qHe4pXKI1oS/fMY1+252sDnc6K40mUHickSXE37MrSRxUsepiHNgdi0DhHM8kX6xqGtx/VqS88CHNy4cb3sFPNvW10p++8kUDGFOYdjijWZr4ExeTlE567OozBDOWxAxQmelKKixtzKjJ790ryim5XaUKVmXemjKUgZMgivlzZJLPTQ91ex+Y+QouqsOBwLUnbXDtjtBjdmWprjZ01Sx1pgqwBDXYRpgHgh7gPRUR4/NTZYDByzyPI7+tGB9lQFSrwv0NVLsk62kDVU2QBgkmNyFMMun2DBvtqH2G0CHl8VXe2uDBPWfoqLZBdmFCKFuQOYl5rUxIIpD4GzG2FX4PV4y0cUIbzP7Kbh87ovWtcWW2ESPnjW3ZiPnbNG7Kmt+a1+rb8lZ5XwjTRf6spiiwRa1IEhQXlnucIdJYlfeXixyOvC57S78isSzqrr2xWRVE78ryNDdT898fwc2q8HY1bmbuweRMbnaQuxzrk0O6md/XzbwT3ax738WWC6GjN3CkHVPbkQbSvIEDHI0CPCDKX73inL6C87UIzpIj17GC4wq7rOBcfYIzf0jBTfQIzjfHnul4wPcc4PBWW36mPwa+DTwT2J7tlunEoWJUVC0Xa5rFaKlZbsbdo2iKDaABV2SvuSQbJ0l0QKn1zVlPVdpJqZKlbvVp32b6tXMb+Oo2nmxwZRtPlqWHK53lBSUMis+tWVzQ82nVu3C9YWnaqz0TMUPxIteBl+dFRxp+jq2TrtSrLjy7V7YBV65yh3vv0lUIf/AsyZX3T2WF6K4DOTlw2+iWZB3SNx5YWipsz3jzueT+5YmevgcSpP78R/HEw6ZxGmuKwzYrr76qKBeA/Q4DtDjMZcoK+XOF7rLi1PNL3eJ7u14YUiRWX5Hs2Oy4zCmq6rThwXqRB5KjuG69dH30Orae0HKMahdArY+vKtGmtnxp8n/EH9qZTT0pku2BcXvLERjeeDIxePA3HNdzXQcc57a2K40r+79mty2nez8ibEmwVuQViVBX2mF7mkRoORcWYdcR3AEq+ruYYYZRqrukH+xUnHTAtOPLblkmnOVUnP3eahLzcsEB9AwOhdYvsUL5R65QcnA4c2Jpe+9MhJdbocy+hbEuEcpSkT+F9dacfAp1sOKXN+v/fyu61/9EaN39Bw== -------------------------------------------------------------------------------- /docs/onion_architecture/onion_architure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/onion_architecture/onion_architure.png -------------------------------------------------------------------------------- /docs/uml/domain-objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/uml/domain-objects.png -------------------------------------------------------------------------------- /docs/uml/uml.md: -------------------------------------------------------------------------------- 1 | # UML Diagrams based on PlantUML 2 | 3 | ## Use Cases 4 | ```plantuml 5 | @startuml usecases 6 | 7 | left to right direction 8 | 9 | actor user as User 10 | usecase (Adding a todo) as UC1 11 | usecase (Get a todo done) as UC2 12 | usecase (Read undone todos) as UC3 13 | 14 | User --> UC1 15 | User --> UC2 16 | User --> UC3 17 | 18 | @enduml 19 | ``` 20 | 21 | ## Domain Objects 22 | ```plantuml 23 | @startuml domain-objects 24 | 25 | class Todo <> { 26 | description: String 27 | done(): void 28 | isDone(): boolean 29 | } 30 | class TodoId <> { 31 | id: UUID 32 | } 33 | class TodoList <> { 34 | todos: List 35 | addTodo(Todo): void 36 | getTodoDone(TodoId): void 37 | undoneTodos(): List 38 | } 39 | class UserId <> { 40 | id: UUID 41 | } 42 | 43 | Todo --> TodoId 44 | TodoList "1" o-- "0..n" Todo 45 | TodoList --> UserId 46 | 47 | hide TodoId methods 48 | hide UserId methods 49 | 50 | @enduml 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /docs/uml/usecases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/docs/uml/usecases.png -------------------------------------------------------------------------------- /gradle/test-acceptance.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | 3 | sourceSets { 4 | acceptanceTest { 5 | groovy { 6 | compileClasspath += main.output + test.output 7 | runtimeClasspath += main.output + test.output 8 | srcDir file('src/test-acceptance/groovy') 9 | } 10 | java { 11 | compileClasspath += main.output + test.output 12 | runtimeClasspath += main.output + test.output 13 | srcDir file('src/test-acceptance/java') 14 | } 15 | resources.srcDir file('src/test-acceptance/resources') 16 | } 17 | } 18 | 19 | configurations { 20 | acceptanceTestCompile.extendsFrom testCompile 21 | acceptanceTestRuntime.extendsFrom testRuntime 22 | } 23 | 24 | idea { 25 | module { 26 | testSourceDirs += project.sourceSets.acceptanceTest.groovy.srcDirs 27 | testSourceDirs += project.sourceSets.acceptanceTest.java.srcDirs 28 | testSourceDirs += project.sourceSets.acceptanceTest.resources.srcDirs 29 | } 30 | } 31 | 32 | task acceptanceTest(type: Test) { 33 | description = 'Runs the acceptance tests.' 34 | group 'verification' 35 | systemProperties = System.properties 36 | testClassesDirs = sourceSets.acceptanceTest.output.classesDirs 37 | classpath = sourceSets.acceptanceTest.runtimeClasspath 38 | } 39 | acceptanceTest.dependsOn test -------------------------------------------------------------------------------- /gradle/test-architecture.gradle: -------------------------------------------------------------------------------- 1 | sourceSets { 2 | architectureTest { 3 | java { 4 | compileClasspath += main.output 5 | runtimeClasspath += main.output 6 | srcDir file('src/test-architecture/java') 7 | } 8 | resources.srcDir file('src/test-architecture/resources') 9 | } 10 | } 11 | 12 | configurations { 13 | architectureTestCompile.extendsFrom compile 14 | architectureTestRuntime.extendsFrom runtime 15 | } 16 | 17 | idea { 18 | module { 19 | testSourceDirs += project.sourceSets.architectureTest.java.srcDirs 20 | testSourceDirs += project.sourceSets.architectureTest.resources.srcDirs 21 | } 22 | } 23 | 24 | task architectureTest(type: Test) { 25 | description = 'Runs the architecture tests.' 26 | group 'verification' 27 | systemProperties = System.properties 28 | testClassesDirs = sourceSets.architectureTest.output.classesDirs 29 | classpath = sourceSets.architectureTest.runtimeClasspath 30 | } 31 | test.finalizedBy architectureTest -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'hexagonal-architecture-example-java' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/common/architecture/Adapter.java: -------------------------------------------------------------------------------- 1 | package common.architecture; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Target(ElementType.TYPE) 7 | public @interface Adapter { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/common/architecture/Command.java: -------------------------------------------------------------------------------- 1 | package common.architecture; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Target; 6 | 7 | @Documented 8 | @Target(ElementType.TYPE) 9 | public @interface Command { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/common/architecture/Port.java: -------------------------------------------------------------------------------- 1 | package common.architecture; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Target(ElementType.TYPE) 7 | public @interface Port { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/common/architecture/UseCase.java: -------------------------------------------------------------------------------- 1 | package common.architecture; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Target(ElementType.TYPE) 7 | public @interface UseCase { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/common/eventbus/EventPublisher.java: -------------------------------------------------------------------------------- 1 | package common.eventbus; 2 | 3 | public interface EventPublisher { 4 | 5 | void publish(E event); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/common/eventbus/EventReceiver.java: -------------------------------------------------------------------------------- 1 | package common.eventbus; 2 | 3 | public interface EventReceiver { 4 | 5 | void receive(E event); 6 | 7 | void subscribe(); 8 | 9 | void unsubscribe(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/common/eventbus/impl/AbstractEventReceiverImpl.java: -------------------------------------------------------------------------------- 1 | package common.eventbus.impl; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import common.eventbus.EventReceiver; 5 | 6 | public abstract class AbstractEventReceiverImpl implements EventReceiver { 7 | 8 | private final EventBus eventBus; 9 | 10 | protected AbstractEventReceiverImpl(final EventBus eventBus){ 11 | this.eventBus = eventBus; 12 | } 13 | 14 | public abstract void receive(final E event); 15 | 16 | @Override 17 | public void subscribe() { 18 | this.eventBus.register(this); 19 | } 20 | 21 | @Override 22 | public void unsubscribe() { 23 | this.eventBus.unregister(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/common/eventbus/impl/EventPublisherImpl.java: -------------------------------------------------------------------------------- 1 | package common.eventbus.impl; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import common.eventbus.EventPublisher; 5 | 6 | public class EventPublisherImpl implements EventPublisher { 7 | 8 | private final EventBus eventBus; 9 | 10 | public EventPublisherImpl(final EventBus eventBus){ 11 | this.eventBus = eventBus; 12 | } 13 | 14 | @Override 15 | public void publish(final E event) { 16 | this.eventBus.post(event); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/todo/Application.java: -------------------------------------------------------------------------------- 1 | package todo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(final String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/events/ReceiveTodoAddedEventGuavaAdapter.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.events; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import com.google.common.eventbus.Subscribe; 5 | import common.architecture.Adapter; 6 | import common.eventbus.impl.AbstractEventReceiverImpl; 7 | import todo.domain.event.TodoAddedEvent; 8 | import todo.domain.port.in.ReceiveTodoAddedEventPort; 9 | 10 | import javax.annotation.PostConstruct; 11 | import javax.annotation.PreDestroy; 12 | import java.util.function.Consumer; 13 | 14 | @Adapter 15 | public class ReceiveTodoAddedEventGuavaAdapter extends AbstractEventReceiverImpl implements ReceiveTodoAddedEventPort { 16 | 17 | private final Consumer eventConsumer; 18 | 19 | public ReceiveTodoAddedEventGuavaAdapter(final EventBus eventBus, final Consumer eventConsumer){ 20 | super(eventBus); 21 | this.eventConsumer = eventConsumer; 22 | subscribeAfterInit(); // TODO should be solved via annotations -> use spring 23 | } 24 | 25 | @PostConstruct 26 | private void subscribeAfterInit(){ 27 | subscribe(); 28 | } 29 | 30 | @PreDestroy 31 | private void unsubscribeBeforeDestroy(){ 32 | unsubscribe(); 33 | } 34 | 35 | @Subscribe 36 | @Override 37 | public void receive(final TodoAddedEvent event) { 38 | this.eventConsumer.accept(event); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/events/ReceiveTodoDoneEventGuavaAdapter.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.events; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import com.google.common.eventbus.Subscribe; 5 | import common.architecture.Adapter; 6 | import common.eventbus.impl.AbstractEventReceiverImpl; 7 | import todo.domain.event.TodoDoneEvent; 8 | import todo.domain.port.in.ReceiveTodoDoneEventPort; 9 | 10 | import javax.annotation.PostConstruct; 11 | import javax.annotation.PreDestroy; 12 | import java.util.function.Consumer; 13 | 14 | @Adapter 15 | public class ReceiveTodoDoneEventGuavaAdapter extends AbstractEventReceiverImpl implements ReceiveTodoDoneEventPort { 16 | 17 | private final Consumer eventConsumer; 18 | 19 | public ReceiveTodoDoneEventGuavaAdapter(final EventBus eventBus, final Consumer eventConsumer){ 20 | super(eventBus); 21 | this.eventConsumer = eventConsumer; 22 | subscribeAfterInit(); // TODO should be solved via annotations -> use spring 23 | } 24 | 25 | @PostConstruct 26 | private void subscribeAfterInit(){ 27 | subscribe(); 28 | } 29 | 30 | @PreDestroy 31 | private void unsubscribeBeforeDestroy(){ 32 | unsubscribe(); 33 | } 34 | 35 | @Subscribe 36 | @Override 37 | public void receive(final TodoDoneEvent event) { 38 | this.eventConsumer.accept(event); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/events/ReceiveTodoListCreatedEventGuavaAdapter.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.events; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import com.google.common.eventbus.Subscribe; 5 | import common.architecture.Adapter; 6 | import common.eventbus.impl.AbstractEventReceiverImpl; 7 | import todo.domain.event.TodoListCreatedEvent; 8 | import todo.domain.port.in.ReceiveTodoListCreatedEventPort; 9 | 10 | import javax.annotation.PostConstruct; 11 | import javax.annotation.PreDestroy; 12 | import java.util.function.Consumer; 13 | 14 | @Adapter 15 | public class ReceiveTodoListCreatedEventGuavaAdapter extends AbstractEventReceiverImpl implements ReceiveTodoListCreatedEventPort { 16 | 17 | private final Consumer eventConsumer; 18 | 19 | public ReceiveTodoListCreatedEventGuavaAdapter(final EventBus eventBus, final Consumer eventConsumer){ 20 | super(eventBus); 21 | this.eventConsumer = eventConsumer; 22 | subscribeAfterInit(); // TODO should be solved via annotations -> use spring 23 | } 24 | 25 | @PostConstruct 26 | private void subscribeAfterInit(){ 27 | subscribe(); 28 | } 29 | 30 | @PreDestroy 31 | private void unsubscribeBeforeDestroy(){ 32 | unsubscribe(); 33 | } 34 | 35 | @Subscribe 36 | @Override 37 | public void receive(final TodoListCreatedEvent event) { 38 | this.eventConsumer.accept(event); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/rest/TodoRestController.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.rest; 2 | 3 | import common.architecture.Adapter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | import todo.adapter.in.rest.model.*; 11 | import todo.application.usecase.AddTodo; 12 | import todo.application.usecase.CreateTodoList; 13 | import todo.application.usecase.GetTodoDone; 14 | import todo.application.usecase.ReadingTodos; 15 | import todo.domain.command.AddTodoCommand; 16 | import todo.domain.command.CreateTodoListCommand; 17 | import todo.domain.command.GetTodoDoneCommand; 18 | import todo.domain.command.ReadTodosCommand; 19 | import todo.domain.exception.MaxNumberOfTodosExceedException; 20 | import todo.domain.exception.TodoListAlreadyExistsException; 21 | import todo.domain.exception.UserDoesNotExistException; 22 | import todo.domain.model.Todo; 23 | import todo.domain.model.TodoId; 24 | 25 | import java.util.List; 26 | import java.util.function.Supplier; 27 | 28 | @Adapter 29 | @RestController 30 | @RequestMapping( value = "/api/todolist" 31 | , consumes = MediaType.APPLICATION_JSON_VALUE 32 | , produces = MediaType.APPLICATION_JSON_VALUE 33 | ) 34 | @Slf4j 35 | public class TodoRestController { 36 | 37 | private final AddTodo addTodoService; 38 | private final CreateTodoList createTodoListService; 39 | private final GetTodoDone getTodoDoneService; 40 | private final ReadingTodos readingTodosService; 41 | 42 | @Autowired 43 | TodoRestController( 44 | final AddTodo addTodo, 45 | final CreateTodoList createTodoList, 46 | final GetTodoDone getTodoDone, 47 | final ReadingTodos readingTodos 48 | ){ 49 | 50 | this.addTodoService = addTodo; 51 | this.createTodoListService = createTodoList; 52 | this.getTodoDoneService = getTodoDone; 53 | this.readingTodosService = readingTodos; 54 | } 55 | 56 | private static ResponseEntity handleRuntimeExceptions(final Supplier> function){ 57 | try { 58 | return function.get(); 59 | } catch (final IllegalArgumentException e) { 60 | log.error("The request seems to be invalid!", e); 61 | return ResponseEntity.badRequest().build(); 62 | } catch (final Exception e) { 63 | log.error("Oops, something went wrong...", e); 64 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 65 | } 66 | } 67 | 68 | @PostMapping(path = "/{userId}/todo") 69 | ResponseEntity addTodo( 70 | @PathVariable(name = "userId") final String userId, 71 | @RequestBody final AddTodoRequest request 72 | ) { 73 | return handleRuntimeExceptions(() -> { 74 | try { 75 | final TodoId todoId = this.addTodoService.addTodo( 76 | AddTodoCommand.of(MappingUtil.userId(userId), request.getDescription()) 77 | ); 78 | return ResponseEntity.ok(new AddTodoResponse(todoId.getId())); 79 | } catch (final MaxNumberOfTodosExceedException e) { 80 | log.error(String.format("The maximum number of todos of user %s is exceed!", userId), e); 81 | return ResponseEntity.unprocessableEntity().build(); 82 | } catch (final UserDoesNotExistException e) { 83 | log.error(String.format("User %s does not exist!", userId), e); 84 | return ResponseEntity.notFound().build(); 85 | } 86 | }); 87 | } 88 | 89 | @PostMapping(path = "") 90 | ResponseEntity createTodolist(@RequestBody final CreateTodolistRequest request) { 91 | return handleRuntimeExceptions(() -> { 92 | try { 93 | this.createTodoListService.createTodoList(CreateTodoListCommand.of(request.toUserId())); 94 | return ResponseEntity.ok().build(); 95 | } catch (final TodoListAlreadyExistsException e) { 96 | log.error(String.format("Todolist for user %s already exists!", request.getUserId()), e); 97 | return ResponseEntity.badRequest().build(); 98 | } 99 | }); 100 | } 101 | 102 | @PostMapping(path = "/{userId}/todo/{todoId}/done") 103 | ResponseEntity getTodoDone( 104 | @PathVariable(name = "userId") final String userId, 105 | @PathVariable(name = "todoId") final String todoId 106 | ) { 107 | return handleRuntimeExceptions(() -> { 108 | try { 109 | this.getTodoDoneService.getTodoDone( 110 | GetTodoDoneCommand.of(MappingUtil.userId(userId), MappingUtil.todoId(todoId)) 111 | ); 112 | return ResponseEntity.ok().build(); 113 | } catch (final UserDoesNotExistException e) { 114 | log.error(String.format("User %s does not exist!", userId), e); 115 | return ResponseEntity.badRequest().build(); 116 | } 117 | }); 118 | } 119 | 120 | 121 | @GetMapping(path = "/{userId}/todo") 122 | ResponseEntity getAllUndoneTodos(@PathVariable(name = "userId") final String userId) { 123 | return handleRuntimeExceptions(() -> { 124 | try { 125 | final List undoneTodos = this.readingTodosService.getAllUndoneTodos(ReadTodosCommand.of(MappingUtil.userId(userId))); 126 | return ResponseEntity.ok(new GetAllUndoneTodosResponse(undoneTodos)); 127 | } catch (final UserDoesNotExistException e) { 128 | log.error(String.format("User %s does not exist!", userId), e); 129 | return ResponseEntity.badRequest().build(); 130 | } 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/rest/model/AddTodoRequest.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.rest.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class AddTodoRequest { 7 | 8 | private String description; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/rest/model/AddTodoResponse.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.rest.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.UUID; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class AddTodoResponse { 13 | 14 | private UUID todoId; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/rest/model/CreateTodolistRequest.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.rest.model; 2 | 3 | import lombok.Data; 4 | import todo.domain.model.UserId; 5 | 6 | @Data 7 | public class CreateTodolistRequest { 8 | 9 | private String userId; 10 | 11 | public UserId toUserId(){ 12 | return MappingUtil.userId(this.userId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/rest/model/GetAllUndoneTodosResponse.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.rest.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import todo.domain.model.Todo; 7 | 8 | import java.util.List; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class GetAllUndoneTodosResponse { 14 | 15 | private List undoneTodos; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/in/rest/model/MappingUtil.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.in.rest.model; 2 | 3 | import todo.domain.model.TodoId; 4 | import todo.domain.model.UserId; 5 | 6 | import java.util.UUID; 7 | 8 | public class MappingUtil { 9 | 10 | public static UUID uuid(final String id){ 11 | return UUID.fromString(id); 12 | } 13 | 14 | public static UserId userId(final String id){ 15 | return UserId.of(uuid(id)); 16 | } 17 | 18 | public static TodoId todoId(String id) { 19 | return TodoId.of(uuid(id)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/out/db/TodoListListInMemoryRepository.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.out.db; 2 | 3 | import common.architecture.Adapter; 4 | import todo.domain.model.TodoList; 5 | import todo.domain.model.UserId; 6 | import todo.domain.port.out.LoadTodoListPort; 7 | import todo.domain.port.out.SaveTodoListPort; 8 | 9 | import java.util.*; 10 | 11 | @Adapter 12 | public class TodoListListInMemoryRepository implements LoadTodoListPort, SaveTodoListPort { 13 | 14 | private final Map repository = new HashMap<>(); 15 | 16 | public TodoListListInMemoryRepository(){} 17 | 18 | @Override 19 | public Collection findAll() { 20 | return Collections.unmodifiableCollection(this.repository.values()); 21 | } 22 | 23 | @Override 24 | public Optional findById(final UserId id) { 25 | return Optional.ofNullable(this.repository.getOrDefault(id, null)); 26 | } 27 | 28 | @Override 29 | public void save(final TodoList list) { 30 | this.repository.put(list.getUserId(), list); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/out/events/SendTodoAddedEventGuavaAdapter.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.out.events; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import common.architecture.Adapter; 5 | import common.eventbus.impl.EventPublisherImpl; 6 | import todo.domain.event.TodoAddedEvent; 7 | import todo.domain.port.out.SendTodoAddedEventPort; 8 | 9 | @Adapter 10 | public class SendTodoAddedEventGuavaAdapter extends EventPublisherImpl implements SendTodoAddedEventPort { 11 | 12 | public SendTodoAddedEventGuavaAdapter(final EventBus eventBus){ 13 | super(eventBus); 14 | } 15 | 16 | @Override 17 | public void send(final TodoAddedEvent event) { 18 | publish(event); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/out/events/SendTodoDoneEventGuavaAdapter.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.out.events; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import common.architecture.Adapter; 5 | import common.eventbus.impl.EventPublisherImpl; 6 | import todo.domain.event.TodoDoneEvent; 7 | import todo.domain.port.out.SendTodoDoneEventPort; 8 | 9 | @Adapter 10 | public class SendTodoDoneEventGuavaAdapter extends EventPublisherImpl implements SendTodoDoneEventPort { 11 | 12 | public SendTodoDoneEventGuavaAdapter(final EventBus eventBus){ 13 | super(eventBus); 14 | } 15 | 16 | @Override 17 | public void send(final TodoDoneEvent event) { 18 | publish(event); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/todo/adapter/out/events/SendTodoListCreatedEventGuavaAdapter.java: -------------------------------------------------------------------------------- 1 | package todo.adapter.out.events; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import common.architecture.Adapter; 5 | import common.eventbus.impl.EventPublisherImpl; 6 | import todo.domain.event.TodoListCreatedEvent; 7 | import todo.domain.port.out.SendTodoListCreatedEventPort; 8 | 9 | @Adapter 10 | public class SendTodoListCreatedEventGuavaAdapter extends EventPublisherImpl implements SendTodoListCreatedEventPort { 11 | 12 | public SendTodoListCreatedEventGuavaAdapter(final EventBus eventBus){ 13 | super(eventBus); 14 | } 15 | 16 | @Override 17 | public void send(final TodoListCreatedEvent event) { 18 | publish(event); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/todo/application/service/AddTodoImpl.java: -------------------------------------------------------------------------------- 1 | package todo.application.service; 2 | 3 | import io.hschwentner.dddbits.annotation.ApplicationService; 4 | import lombok.NonNull; 5 | import todo.domain.command.AddTodoCommand; 6 | import todo.application.usecase.AddTodo; 7 | import todo.domain.event.TodoAddedEvent; 8 | import todo.domain.exception.MaxNumberOfTodosExceedException; 9 | import todo.domain.exception.UserDoesNotExistException; 10 | import todo.domain.model.Todo; 11 | import todo.domain.model.TodoId; 12 | import todo.domain.model.TodoList; 13 | import todo.domain.model.UserId; 14 | import todo.domain.port.out.LoadTodoListPort; 15 | import todo.domain.port.out.SaveTodoListPort; 16 | import todo.domain.port.out.SendTodoAddedEventPort; 17 | 18 | import java.util.Optional; 19 | 20 | @ApplicationService 21 | public class AddTodoImpl implements AddTodo { 22 | 23 | private static final int MAX_NUMBER_OF_TODOS = 5; // application specific rule 24 | 25 | private final LoadTodoListPort loadTodoListPort; 26 | private final SaveTodoListPort saveTodoListPort; 27 | private final SendTodoAddedEventPort sendTodoAddedEventPort; 28 | 29 | public AddTodoImpl( 30 | final LoadTodoListPort loadTodoListPort, 31 | final SaveTodoListPort saveTodoListPort, 32 | final SendTodoAddedEventPort sendTodoAddedEventPort 33 | ) { 34 | this.loadTodoListPort = loadTodoListPort; 35 | this.saveTodoListPort = saveTodoListPort; 36 | this.sendTodoAddedEventPort = sendTodoAddedEventPort; 37 | } 38 | 39 | @Override 40 | public TodoId addTodo(@NonNull final AddTodoCommand command) throws MaxNumberOfTodosExceedException, UserDoesNotExistException { 41 | final TodoList todoList = todoList(command.getUserId()); 42 | checkIfMaxNumberOfUndoneTodosExceeded(todoList.countUndoneTodos()); 43 | 44 | final Todo newTodo = Todo.create(command.getDescripton()); 45 | todoList.addTodo(newTodo); 46 | 47 | this.saveTodoListPort.save(todoList); 48 | publishTodoAddedEvent( new TodoAddedEvent(newTodo, command.getUserId()) ); 49 | 50 | return newTodo.getId(); 51 | } 52 | 53 | private TodoList todoList(final UserId userId) throws UserDoesNotExistException { 54 | final Optional opt = this.loadTodoListPort.findById(userId); 55 | if (opt.isEmpty()) { 56 | throw new UserDoesNotExistException(userId); 57 | } 58 | return opt.get(); 59 | } 60 | 61 | private void publishTodoAddedEvent(@NonNull final TodoAddedEvent event) { 62 | this.sendTodoAddedEventPort.send(event); 63 | } 64 | 65 | private static void checkIfMaxNumberOfUndoneTodosExceeded(final int numberOfUndoneTodos) throws MaxNumberOfTodosExceedException { 66 | if( numberOfUndoneTodos >= MAX_NUMBER_OF_TODOS ) 67 | throw new MaxNumberOfTodosExceedException(MAX_NUMBER_OF_TODOS); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/todo/application/service/CreateTodoListImpl.java: -------------------------------------------------------------------------------- 1 | package todo.application.service; 2 | 3 | import io.hschwentner.dddbits.annotation.ApplicationService; 4 | import lombok.NonNull; 5 | import todo.domain.command.CreateTodoListCommand; 6 | import todo.application.usecase.CreateTodoList; 7 | import todo.domain.event.TodoListCreatedEvent; 8 | import todo.domain.exception.TodoListAlreadyExistsException; 9 | import todo.domain.model.TodoList; 10 | import todo.domain.model.UserId; 11 | import todo.domain.port.out.LoadTodoListPort; 12 | import todo.domain.port.out.SaveTodoListPort; 13 | import todo.domain.port.out.SendTodoListCreatedEventPort; 14 | 15 | @ApplicationService 16 | public class CreateTodoListImpl implements CreateTodoList { 17 | 18 | private final LoadTodoListPort loadTodoListPort; 19 | private final SaveTodoListPort saveTodoListPort; 20 | private final SendTodoListCreatedEventPort sendTodoListCreatedEventPort; 21 | 22 | public CreateTodoListImpl(final LoadTodoListPort loadTodoListPort, final SaveTodoListPort saveTodoListPort, final SendTodoListCreatedEventPort sendTodoListCreatedEventPort){ 23 | this.loadTodoListPort = loadTodoListPort; 24 | this.saveTodoListPort = saveTodoListPort; 25 | this.sendTodoListCreatedEventPort = sendTodoListCreatedEventPort; 26 | } 27 | 28 | @Override 29 | public void createTodoList(@NonNull final CreateTodoListCommand command) throws TodoListAlreadyExistsException { 30 | checkIfUserHasNoTodoListYet(command.getUserId()); 31 | 32 | final TodoList newTodoList = TodoList.create(command.getUserId()); 33 | 34 | this.saveTodoListPort.save(newTodoList); 35 | publishTodoListCreatedEvent(new TodoListCreatedEvent(command.getUserId())); 36 | } 37 | 38 | private void checkIfUserHasNoTodoListYet(@NonNull final UserId userId) throws TodoListAlreadyExistsException { 39 | if (this.loadTodoListPort.findById(userId).isPresent()) { 40 | throw new TodoListAlreadyExistsException(userId); 41 | } 42 | } 43 | 44 | private void publishTodoListCreatedEvent(final TodoListCreatedEvent event) { 45 | this.sendTodoListCreatedEventPort.send(event); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/todo/application/service/GetTodoDoneImpl.java: -------------------------------------------------------------------------------- 1 | package todo.application.service; 2 | 3 | import io.hschwentner.dddbits.annotation.ApplicationService; 4 | import todo.application.usecase.GetTodoDone; 5 | import todo.domain.command.GetTodoDoneCommand; 6 | import todo.domain.event.TodoDoneEvent; 7 | import todo.domain.exception.UserDoesNotExistException; 8 | import todo.domain.model.TodoList; 9 | import todo.domain.model.UserId; 10 | import todo.domain.port.out.LoadTodoListPort; 11 | import todo.domain.port.out.SaveTodoListPort; 12 | import todo.domain.port.out.SendTodoDoneEventPort; 13 | 14 | import java.util.Optional; 15 | 16 | @ApplicationService 17 | public class GetTodoDoneImpl implements GetTodoDone { 18 | 19 | private final LoadTodoListPort loadTodoListPort; 20 | private final SaveTodoListPort saveTodoListPort; 21 | private final SendTodoDoneEventPort sendTodoDoneEventPort; 22 | 23 | public GetTodoDoneImpl(final LoadTodoListPort loadTodoListPort, final SaveTodoListPort saveTodoListPort, final SendTodoDoneEventPort sendTodoDoneEventPort){ 24 | this.loadTodoListPort = loadTodoListPort; 25 | this.saveTodoListPort = saveTodoListPort; 26 | this.sendTodoDoneEventPort = sendTodoDoneEventPort; 27 | } 28 | 29 | @Override 30 | public void getTodoDone(final GetTodoDoneCommand command) throws UserDoesNotExistException { 31 | final TodoList todoList = todoList(command.getUserId()); 32 | 33 | todoList.getTodoDone(command.getTodoId()); 34 | 35 | this.saveTodoListPort.save(todoList); 36 | publishTodoDoneEvent(new TodoDoneEvent(command.getUserId(), command.getTodoId()) ); 37 | } 38 | 39 | private void publishTodoDoneEvent(final TodoDoneEvent event) { 40 | this.sendTodoDoneEventPort.send(event); 41 | } 42 | 43 | private TodoList todoList(final UserId userId) throws UserDoesNotExistException { 44 | final Optional opt = this.loadTodoListPort.findById(userId); 45 | if( opt.isEmpty() ) 46 | throw new UserDoesNotExistException(userId); 47 | return opt.get(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/todo/application/service/ReadingTodosImpl.java: -------------------------------------------------------------------------------- 1 | package todo.application.service; 2 | 3 | import io.hschwentner.dddbits.annotation.ApplicationService; 4 | import lombok.NonNull; 5 | import todo.domain.command.ReadTodosCommand; 6 | import todo.application.usecase.ReadingTodos; 7 | import todo.domain.event.TodoAddedEvent; 8 | import todo.domain.event.TodoDoneEvent; 9 | import todo.domain.event.TodoListCreatedEvent; 10 | import todo.domain.exception.UserDoesNotExistException; 11 | import todo.domain.model.Todo; 12 | import todo.domain.model.UserId; 13 | import todo.domain.port.out.LoadTodoListPort; 14 | 15 | import java.util.*; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | import java.util.stream.Collectors; 18 | 19 | @ApplicationService 20 | public class ReadingTodosImpl implements ReadingTodos { 21 | 22 | private final Map> undoneTodoRepository = new ConcurrentHashMap<>(); 23 | 24 | private final LoadTodoListPort loadTodoListPort; 25 | 26 | public ReadingTodosImpl(final LoadTodoListPort loadTodoListPort){ 27 | this.loadTodoListPort = loadTodoListPort; 28 | initUndoneTodoRepository(); // init state 29 | } 30 | 31 | private void initUndoneTodoRepository(){ 32 | this.loadTodoListPort.findAll().forEach(list -> 33 | this.undoneTodoRepository.put(list.getUserId(), list.undoneTodos()) 34 | ); 35 | } 36 | 37 | @Override 38 | public List getAllUndoneTodos(@NonNull final ReadTodosCommand command) throws UserDoesNotExistException { 39 | initUndoneTodoRepository(); // TODO remove this when using an event bus 40 | if( !this.undoneTodoRepository.containsKey(command.getUserId()) ) { 41 | throw new UserDoesNotExistException(command.getUserId()); 42 | } 43 | return Collections.unmodifiableList(this.undoneTodoRepository.get(command.getUserId())); 44 | } 45 | 46 | public void on(final TodoAddedEvent event) { 47 | this.undoneTodoRepository.get(event.getUserId()).add(event.getTodo()); 48 | } 49 | 50 | public void on(final TodoDoneEvent event) { 51 | final List todos = this.undoneTodoRepository.get(event.getUserId()); 52 | this.undoneTodoRepository.put(event.getUserId(), todos.stream() 53 | .filter(t -> !t.getId().equals(event.getTodoId()) ) 54 | .collect(Collectors.toList()) ); 55 | } 56 | 57 | public void on(final TodoListCreatedEvent event) { 58 | this.undoneTodoRepository.put(event.getUserId(), new LinkedList<>()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/todo/application/usecase/AddTodo.java: -------------------------------------------------------------------------------- 1 | package todo.application.usecase; 2 | 3 | import common.architecture.UseCase; 4 | import todo.domain.command.AddTodoCommand; 5 | import todo.domain.exception.MaxNumberOfTodosExceedException; 6 | import todo.domain.exception.UserDoesNotExistException; 7 | import todo.domain.model.TodoId; 8 | 9 | @UseCase 10 | public interface AddTodo { 11 | 12 | TodoId addTodo(AddTodoCommand command) throws MaxNumberOfTodosExceedException, UserDoesNotExistException; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/todo/application/usecase/CreateTodoList.java: -------------------------------------------------------------------------------- 1 | package todo.application.usecase; 2 | 3 | import common.architecture.UseCase; 4 | import todo.domain.command.CreateTodoListCommand; 5 | import todo.domain.exception.TodoListAlreadyExistsException; 6 | 7 | @UseCase 8 | public interface CreateTodoList { 9 | 10 | void createTodoList(CreateTodoListCommand command) throws TodoListAlreadyExistsException; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/todo/application/usecase/GetTodoDone.java: -------------------------------------------------------------------------------- 1 | package todo.application.usecase; 2 | 3 | import common.architecture.UseCase; 4 | import todo.domain.command.GetTodoDoneCommand; 5 | import todo.domain.exception.UserDoesNotExistException; 6 | 7 | @UseCase 8 | public interface GetTodoDone { 9 | 10 | void getTodoDone(GetTodoDoneCommand command) throws UserDoesNotExistException; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/todo/application/usecase/ReadingTodos.java: -------------------------------------------------------------------------------- 1 | package todo.application.usecase; 2 | 3 | import common.architecture.UseCase; 4 | import todo.domain.command.ReadTodosCommand; 5 | import todo.domain.exception.UserDoesNotExistException; 6 | import todo.domain.model.Todo; 7 | 8 | import java.util.List; 9 | 10 | @UseCase 11 | public interface ReadingTodos { 12 | 13 | List getAllUndoneTodos(ReadTodosCommand command) throws UserDoesNotExistException; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/todo/config/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package todo.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import todo.application.service.AddTodoImpl; 6 | import todo.application.service.CreateTodoListImpl; 7 | import todo.application.service.GetTodoDoneImpl; 8 | import todo.application.service.ReadingTodosImpl; 9 | import todo.application.usecase.AddTodo; 10 | import todo.application.usecase.CreateTodoList; 11 | import todo.application.usecase.GetTodoDone; 12 | import todo.application.usecase.ReadingTodos; 13 | import todo.domain.port.out.*; 14 | 15 | @Configuration 16 | public class ApplicationConfig { 17 | 18 | @Bean 19 | AddTodo addTodo(final LoadTodoListPort loadTodoListPort, final SaveTodoListPort saveTodoListPort, final SendTodoAddedEventPort sendTodoAddedEventPort){ 20 | return new AddTodoImpl(loadTodoListPort, saveTodoListPort, sendTodoAddedEventPort); 21 | } 22 | 23 | @Bean 24 | CreateTodoList createTodoList(final LoadTodoListPort loadTodoListPort, final SaveTodoListPort saveTodoListPort, final SendTodoListCreatedEventPort sendTodoListCreatedEventPort){ 25 | return new CreateTodoListImpl(loadTodoListPort, saveTodoListPort, sendTodoListCreatedEventPort); 26 | } 27 | 28 | @Bean 29 | GetTodoDone getTodoDone(final LoadTodoListPort loadTodoListPort, final SaveTodoListPort saveTodoListPort, final SendTodoDoneEventPort sendTodoDoneEventPort){ 30 | return new GetTodoDoneImpl(loadTodoListPort, saveTodoListPort, sendTodoDoneEventPort); 31 | } 32 | 33 | @Bean 34 | ReadingTodos readingTodos(final LoadTodoListPort loadTodoListPort){ 35 | return new ReadingTodosImpl(loadTodoListPort); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/todo/config/InfrastructureConfig.java: -------------------------------------------------------------------------------- 1 | package todo.config; 2 | 3 | import com.google.common.eventbus.EventBus; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import todo.adapter.out.db.TodoListListInMemoryRepository; 7 | import todo.adapter.out.events.SendTodoAddedEventGuavaAdapter; 8 | import todo.adapter.out.events.SendTodoDoneEventGuavaAdapter; 9 | import todo.adapter.out.events.SendTodoListCreatedEventGuavaAdapter; 10 | import todo.domain.port.out.*; 11 | 12 | @Configuration 13 | public class InfrastructureConfig { 14 | 15 | private final EventBus eventBus; 16 | private final TodoListListInMemoryRepository repository; 17 | 18 | public InfrastructureConfig(){ 19 | this.eventBus = new EventBus(); 20 | this.repository = new TodoListListInMemoryRepository(); 21 | } 22 | 23 | @Bean 24 | LoadTodoListPort loadTodoListPort(){ 25 | return this.repository; 26 | } 27 | 28 | @Bean 29 | SaveTodoListPort saveTodoListPort(){ 30 | return this.repository; 31 | } 32 | 33 | @Bean 34 | SendTodoAddedEventPort sendTodoAddedEventPort() { 35 | return new SendTodoAddedEventGuavaAdapter(this.eventBus); 36 | } 37 | 38 | @Bean 39 | SendTodoListCreatedEventPort sendTodoListCreatedEventPort() { 40 | return new SendTodoListCreatedEventGuavaAdapter(this.eventBus); 41 | } 42 | 43 | @Bean 44 | SendTodoDoneEventPort sendTodoDoneEventPort() { 45 | return new SendTodoDoneEventGuavaAdapter(this.eventBus); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/command/AddTodoCommand.java: -------------------------------------------------------------------------------- 1 | package todo.domain.command; 2 | 3 | import common.architecture.Command; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | import todo.domain.model.UserId; 7 | 8 | @Command 9 | @Value(staticConstructor = "of") 10 | public class AddTodoCommand { 11 | 12 | @NonNull UserId userId; 13 | @NonNull String descripton; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/command/CreateTodoListCommand.java: -------------------------------------------------------------------------------- 1 | package todo.domain.command; 2 | 3 | import common.architecture.Command; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | import todo.domain.model.UserId; 7 | 8 | @Command 9 | @Value(staticConstructor = "of") 10 | public class CreateTodoListCommand { 11 | 12 | @NonNull UserId userId; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/command/GetTodoDoneCommand.java: -------------------------------------------------------------------------------- 1 | package todo.domain.command; 2 | 3 | import common.architecture.Command; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | import todo.domain.model.TodoId; 7 | import todo.domain.model.UserId; 8 | 9 | @Command 10 | @Value(staticConstructor = "of") 11 | public class GetTodoDoneCommand { 12 | 13 | @NonNull UserId userId; 14 | @NonNull TodoId todoId; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/command/ReadTodosCommand.java: -------------------------------------------------------------------------------- 1 | package todo.domain.command; 2 | 3 | import common.architecture.Command; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | import todo.domain.model.UserId; 7 | 8 | @Command 9 | @Value(staticConstructor = "of") 10 | public class ReadTodosCommand { 11 | 12 | @NonNull UserId userId; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/event/TodoAddedEvent.java: -------------------------------------------------------------------------------- 1 | package todo.domain.event; 2 | 3 | import io.hschwentner.dddbits.annotation.DomainEvent; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | import todo.domain.model.Todo; 7 | import todo.domain.model.UserId; 8 | 9 | @DomainEvent 10 | @Value 11 | public class TodoAddedEvent { 12 | 13 | @NonNull Todo todo; 14 | @NonNull UserId userId; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/event/TodoDoneEvent.java: -------------------------------------------------------------------------------- 1 | package todo.domain.event; 2 | 3 | import io.hschwentner.dddbits.annotation.DomainEvent; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | import todo.domain.model.TodoId; 7 | import todo.domain.model.UserId; 8 | 9 | @DomainEvent 10 | @Value 11 | public class TodoDoneEvent { 12 | 13 | @NonNull UserId userId; 14 | @NonNull TodoId todoId; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/event/TodoListCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package todo.domain.event; 2 | 3 | import io.hschwentner.dddbits.annotation.DomainEvent; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | import todo.domain.model.UserId; 7 | 8 | @DomainEvent 9 | @Value 10 | public class TodoListCreatedEvent { 11 | 12 | @NonNull UserId userId; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/exception/MaxNumberOfTodosExceedException.java: -------------------------------------------------------------------------------- 1 | package todo.domain.exception; 2 | 3 | public class MaxNumberOfTodosExceedException extends Exception { 4 | 5 | public MaxNumberOfTodosExceedException(final int max){ 6 | super(String.format("The maximum number (%d) of todos is exceeded!", max)); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/exception/TodoListAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package todo.domain.exception; 2 | 3 | import todo.domain.model.UserId; 4 | 5 | public class TodoListAlreadyExistsException extends Exception { 6 | 7 | public TodoListAlreadyExistsException(final UserId userId) { 8 | super(String.format("Todo list for user with userId='%s' already exists!", userId.getId().toString())); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/exception/UserDoesNotExistException.java: -------------------------------------------------------------------------------- 1 | package todo.domain.exception; 2 | 3 | import todo.domain.model.UserId; 4 | 5 | public class UserDoesNotExistException extends Exception { 6 | 7 | public UserDoesNotExistException(final UserId id) { 8 | super(String.format("User with id='%s' does not exist!", id.getId().toString())); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/model/Todo.java: -------------------------------------------------------------------------------- 1 | package todo.domain.model; 2 | 3 | import io.hschwentner.dddbits.annotation.DomainEntity; 4 | import lombok.AccessLevel; 5 | import lombok.Data; 6 | import lombok.NonNull; 7 | import lombok.Setter; 8 | 9 | @DomainEntity 10 | @Data 11 | public class Todo { 12 | 13 | private final TodoId id; 14 | private final String description; 15 | @Setter(AccessLevel.PRIVATE) 16 | private boolean isDone; 17 | 18 | public Todo(final String description){ 19 | this(TodoId.create(), description); 20 | } 21 | 22 | public Todo(@NonNull final TodoId id, @NonNull final String description){ 23 | this.id = id; 24 | this.description = description; 25 | this.isDone = false; 26 | } 27 | 28 | public static Todo create(final String description){ 29 | return new Todo(TodoId.create(), description); 30 | } 31 | 32 | public void done(){ 33 | setDone(true); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/model/TodoId.java: -------------------------------------------------------------------------------- 1 | package todo.domain.model; 2 | 3 | import io.hschwentner.dddbits.annotation.ValueObject; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | 7 | import java.util.UUID; 8 | 9 | @ValueObject 10 | @Value 11 | public class TodoId { 12 | 13 | private final UUID id; 14 | 15 | private TodoId(@NonNull final UUID id){ 16 | this.id = id; 17 | } 18 | 19 | public static TodoId create(){ 20 | return of(UUID.randomUUID()); 21 | } 22 | 23 | public static TodoId of(final UUID id){ 24 | return new TodoId(id); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/model/TodoList.java: -------------------------------------------------------------------------------- 1 | package todo.domain.model; 2 | 3 | import io.hschwentner.dddbits.annotation.AggregateRoot; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NonNull; 7 | import lombok.Value; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Optional; 12 | import java.util.stream.Collectors; 13 | 14 | @AggregateRoot 15 | @Value 16 | public class TodoList { 17 | 18 | @Getter(AccessLevel.PRIVATE) 19 | private final List todos = new ArrayList<>(); 20 | private final UserId userId; 21 | 22 | private TodoList(@NonNull final UserId userId){ 23 | this.userId = userId; 24 | } 25 | 26 | public static TodoList create(final UserId userId){ 27 | return new TodoList(userId); 28 | } 29 | 30 | public void addTodo(@NonNull final Todo todo) { 31 | this.todos.add(todo); 32 | } 33 | 34 | public void getTodoDone(@NonNull final TodoId todoId) { 35 | findById(todoId).ifPresent(t -> t.done() ); 36 | } 37 | 38 | public List undoneTodos() { 39 | return this.todos.stream() 40 | .filter(t -> !t.isDone()) 41 | .collect(Collectors.toList()); 42 | } 43 | 44 | public int countUndoneTodos(){ 45 | return undoneTodos().size(); 46 | } 47 | 48 | private Optional findById(final TodoId id){ 49 | return this.todos.stream() 50 | .filter(t -> t.getId().equals(id)) 51 | .findFirst(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/model/UserId.java: -------------------------------------------------------------------------------- 1 | package todo.domain.model; 2 | 3 | import io.hschwentner.dddbits.annotation.ValueObject; 4 | import lombok.NonNull; 5 | import lombok.Value; 6 | 7 | import java.util.UUID; 8 | 9 | @ValueObject 10 | @Value 11 | public class UserId { 12 | 13 | private final UUID id; 14 | 15 | private UserId(@NonNull final UUID id){ 16 | this.id = id; 17 | } 18 | 19 | public static UserId create(){ 20 | return of(UUID.randomUUID()); 21 | } 22 | 23 | public static UserId of(@NonNull final UUID id){ 24 | return new UserId(id); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/in/ReceiveTodoAddedEventPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.in; 2 | 3 | import common.architecture.Port; 4 | import todo.domain.event.TodoAddedEvent; 5 | 6 | @Port 7 | public interface ReceiveTodoAddedEventPort { 8 | 9 | void receive(TodoAddedEvent event); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/in/ReceiveTodoDoneEventPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.in; 2 | 3 | import common.architecture.Port; 4 | import todo.domain.event.TodoDoneEvent; 5 | 6 | @Port 7 | public interface ReceiveTodoDoneEventPort { 8 | 9 | void receive(TodoDoneEvent event); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/in/ReceiveTodoListCreatedEventPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.in; 2 | 3 | import common.architecture.Port; 4 | import todo.domain.event.TodoListCreatedEvent; 5 | 6 | @Port 7 | public interface ReceiveTodoListCreatedEventPort { 8 | 9 | void receive(TodoListCreatedEvent event); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/out/LoadTodoListPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.out; 2 | 3 | import java.util.Collection; 4 | import java.util.Optional; 5 | 6 | import common.architecture.Port; 7 | import todo.domain.model.TodoList; 8 | import todo.domain.model.UserId; 9 | 10 | @Port 11 | public interface LoadTodoListPort { 12 | 13 | Collection findAll(); 14 | 15 | Optional findById(UserId id); 16 | } -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/out/SaveTodoListPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.out; 2 | 3 | import common.architecture.Port; 4 | import todo.domain.model.TodoList; 5 | 6 | @Port 7 | public interface SaveTodoListPort { 8 | 9 | void save(TodoList list); 10 | } -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/out/SendTodoAddedEventPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.out; 2 | 3 | import common.architecture.Port; 4 | import todo.domain.event.TodoAddedEvent; 5 | 6 | @Port 7 | public interface SendTodoAddedEventPort { 8 | 9 | void send(TodoAddedEvent event); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/out/SendTodoDoneEventPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.out; 2 | 3 | import common.architecture.Port; 4 | import todo.domain.event.TodoDoneEvent; 5 | 6 | @Port 7 | public interface SendTodoDoneEventPort { 8 | 9 | void send(TodoDoneEvent event); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todo/domain/port/out/SendTodoListCreatedEventPort.java: -------------------------------------------------------------------------------- 1 | package todo.domain.port.out; 2 | 3 | import common.architecture.Port; 4 | import todo.domain.event.TodoListCreatedEvent; 5 | 6 | @Port 7 | public interface SendTodoListCreatedEventPort { 8 | 9 | void send(TodoListCreatedEvent event); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sneufeind/hexagonal-architecture-example-java/d72ed2993e69480da040795a889ba9e526dd053b/src/main/resources/application.yml -------------------------------------------------------------------------------- /src/test-acceptance/java/todo/ToDoStepDefs.java: -------------------------------------------------------------------------------- 1 | package todo; 2 | 3 | import com.google.common.eventbus.DeadEvent; 4 | import com.google.common.eventbus.EventBus; 5 | import com.google.common.eventbus.Subscribe; 6 | import io.cucumber.datatable.DataTable; 7 | import io.cucumber.java.en.And; 8 | import io.cucumber.java.en.Given; 9 | import io.cucumber.java.en.Then; 10 | import io.cucumber.java.en.When; 11 | import todo.adapter.in.events.ReceiveTodoAddedEventGuavaAdapter; 12 | import todo.adapter.in.events.ReceiveTodoDoneEventGuavaAdapter; 13 | import todo.adapter.in.events.ReceiveTodoListCreatedEventGuavaAdapter; 14 | import todo.adapter.out.db.TodoListListInMemoryRepository; 15 | import todo.adapter.out.events.SendTodoAddedEventGuavaAdapter; 16 | import todo.adapter.out.events.SendTodoDoneEventGuavaAdapter; 17 | import todo.adapter.out.events.SendTodoListCreatedEventGuavaAdapter; 18 | import todo.application.service.AddTodoImpl; 19 | import todo.application.service.CreateTodoListImpl; 20 | import todo.application.service.GetTodoDoneImpl; 21 | import todo.application.service.ReadingTodosImpl; 22 | import todo.application.usecase.AddTodo; 23 | import todo.application.usecase.CreateTodoList; 24 | import todo.application.usecase.GetTodoDone; 25 | import todo.domain.command.AddTodoCommand; 26 | import todo.domain.command.CreateTodoListCommand; 27 | import todo.domain.command.GetTodoDoneCommand; 28 | import todo.domain.command.ReadTodosCommand; 29 | import todo.domain.exception.MaxNumberOfTodosExceedException; 30 | import todo.domain.exception.TodoListAlreadyExistsException; 31 | import todo.domain.exception.UserDoesNotExistException; 32 | import todo.domain.model.Todo; 33 | import todo.domain.model.TodoId; 34 | import todo.domain.model.TodoList; 35 | import todo.domain.model.UserId; 36 | import todo.domain.port.in.ReceiveTodoAddedEventPort; 37 | import todo.domain.port.in.ReceiveTodoDoneEventPort; 38 | import todo.domain.port.in.ReceiveTodoListCreatedEventPort; 39 | import todo.domain.port.out.SendTodoAddedEventPort; 40 | import todo.domain.port.out.SendTodoDoneEventPort; 41 | import todo.domain.port.out.SendTodoListCreatedEventPort; 42 | 43 | import java.util.concurrent.atomic.AtomicInteger; 44 | import java.util.function.Function; 45 | 46 | import static org.junit.Assert.*; 47 | 48 | public class ToDoStepDefs { 49 | 50 | private final AtomicInteger deadEventCounter = new AtomicInteger(0); 51 | private final TodoListListInMemoryRepository todoListRepository; 52 | private final AddTodo addTodoUseCase; 53 | private final CreateTodoList createTodoListUseCase; 54 | private final GetTodoDone getTodoDoneUseCase; 55 | private final ReadingTodosImpl readingTodoUseCase; 56 | private final UserId userId; 57 | 58 | private Todo todo; 59 | 60 | public ToDoStepDefs(){ 61 | final EventBus eventBus = new EventBus(); 62 | eventBus.register(this); // for dead events 63 | 64 | this.todoListRepository = new TodoListListInMemoryRepository(); 65 | 66 | this.readingTodoUseCase = new ReadingTodosImpl(this.todoListRepository); 67 | 68 | final SendTodoAddedEventPort sendTodoAddedEventPort = new SendTodoAddedEventGuavaAdapter(eventBus); 69 | final SendTodoDoneEventPort sendTodoDoneEventPort = new SendTodoDoneEventGuavaAdapter(eventBus); 70 | final SendTodoListCreatedEventPort sendTodoListCreatedEventPort = new SendTodoListCreatedEventGuavaAdapter(eventBus); 71 | 72 | final ReceiveTodoAddedEventPort receiveTodoAddedEventPort = new ReceiveTodoAddedEventGuavaAdapter(eventBus, this.readingTodoUseCase::on); 73 | final ReceiveTodoDoneEventPort receiveTodoDoneEventPort = new ReceiveTodoDoneEventGuavaAdapter(eventBus, this.readingTodoUseCase::on); 74 | final ReceiveTodoListCreatedEventPort receiveTodoListCreatedEventPort = new ReceiveTodoListCreatedEventGuavaAdapter(eventBus, this.readingTodoUseCase::on); 75 | 76 | this.addTodoUseCase = new AddTodoImpl(this.todoListRepository, this.todoListRepository, sendTodoAddedEventPort); 77 | this.createTodoListUseCase = new CreateTodoListImpl(this.todoListRepository, this.todoListRepository, sendTodoListCreatedEventPort); 78 | this.getTodoDoneUseCase = new GetTodoDoneImpl(this.todoListRepository, this.todoListRepository, sendTodoDoneEventPort); 79 | this.userId = UserId.create(); 80 | } 81 | 82 | @Subscribe 83 | public void on(final DeadEvent event){ 84 | this.deadEventCounter.incrementAndGet(); 85 | } 86 | 87 | @Given("an empty list") 88 | public void anEmptyList() throws TodoListAlreadyExistsException, UserDoesNotExistException { 89 | initTodoList(this.userId); 90 | assertTrue(this.readingTodoUseCase.getAllUndoneTodos(ReadTodosCommand.of(this.userId)).isEmpty()); 91 | } 92 | 93 | @Given("a list with the following todos:") 94 | public void aListWithTheFollowingTodos(final DataTable table) { 95 | this.todoListRepository.save(TodoList.create(this.userId)); 96 | table.asList().forEach(data -> { 97 | try { 98 | this.addTodoUseCase.addTodo(AddTodoCommand.of(this.userId, data)); 99 | } catch (final MaxNumberOfTodosExceedException | UserDoesNotExistException e) { 100 | throw new RuntimeException(e); 101 | } 102 | }); 103 | assertNumberOfToDos(table.asList().size()); 104 | } 105 | 106 | @When("user adds a new todo") 107 | public void userAddsANewTodo() throws MaxNumberOfTodosExceedException, UserDoesNotExistException { 108 | this.todo = findById( this.addTodoUseCase.addTodo(AddTodoCommand.of(this.userId, "Create a good Example")) ); 109 | } 110 | 111 | @And("tries to add this todo again") 112 | public void triesToAddThisTodoAgain() throws MaxNumberOfTodosExceedException, UserDoesNotExistException { 113 | this.addTodoUseCase.addTodo(AddTodoCommand.of(this.userId, this.todo.getDescription())); 114 | } 115 | 116 | @When("the user asks for his todos") 117 | public void theUserAsksForHisTodos() throws UserDoesNotExistException { 118 | this.readingTodoUseCase.getAllUndoneTodos(ReadTodosCommand.of(this.userId)); 119 | } 120 | 121 | @When("the todo {string} is done") 122 | public void theTodoIsDone(final String todoDescription) throws UserDoesNotExistException { 123 | final Todo todo = findByDescription(todoDescription); 124 | this.getTodoDoneUseCase.getTodoDone(GetTodoDoneCommand.of(this.userId, todo.getId())); 125 | } 126 | 127 | @Then("this todo will be added to the list") 128 | public void thisTodoWillBeAddedToTheList() { 129 | assertNotNull( findById(this.todo.getId()) ); 130 | ensureNoDomainEventsHadBeenIgnored(); 131 | } 132 | 133 | @Then("this todo will be added twice") 134 | public void thisTodoWillBeAddedTwice() { 135 | final TodoList todoList = this.todoListRepository.findById(this.userId).get(); 136 | final long count = todoList.undoneTodos().stream() 137 | .filter(e -> e.getDescription().equals(this.todo.getDescription())) 138 | .count(); 139 | assertEquals(2, count); 140 | ensureNoDomainEventsHadBeenIgnored(); 141 | } 142 | 143 | @Then("the list contains {int} todos") 144 | public void theListContainsTodos(final int expectedNumberOfToDos) { 145 | assertNumberOfToDos(expectedNumberOfToDos); 146 | ensureNoDomainEventsHadBeenIgnored(); 147 | } 148 | 149 | @And("this todo will be unchecked") 150 | public void thisTodoWillBeUnchecked() { 151 | assertFalse(this.todo.isDone()); 152 | } 153 | 154 | private void ensureNoDomainEventsHadBeenIgnored(){ 155 | assertEquals(0, this.deadEventCounter.get()); 156 | } 157 | 158 | private void initTodoList(final UserId userId) throws TodoListAlreadyExistsException { 159 | this.createTodoListUseCase.createTodoList(CreateTodoListCommand.of(userId)); 160 | assertTrue(this.todoListRepository.findById(userId).isPresent()); 161 | } 162 | 163 | private Todo findById(final TodoId id){ 164 | return findBy(t -> t.getId().equals(id)); 165 | } 166 | 167 | private Todo findByDescription(final String description){ 168 | return findBy(t -> t.getDescription().equals(description)); 169 | } 170 | 171 | private Todo findBy(final Function filter){ 172 | return this.todoListRepository.findAll().stream() 173 | .flatMap(list -> list.undoneTodos().stream()) 174 | .filter(t -> filter.apply(t)) 175 | .findFirst() 176 | .get(); 177 | } 178 | 179 | private void assertNumberOfToDos(final int expectedNumber){ 180 | assertEquals(expectedNumber, this.todoListRepository.findById(this.userId).get().countUndoneTodos()); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/test-acceptance/java/todo/TodoUseCaseAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package todo; 2 | 3 | import io.cucumber.junit.Cucumber; 4 | import io.cucumber.junit.CucumberOptions; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions( 9 | features = "src/test-acceptance/resources/todo", 10 | glue = "todo", 11 | plugin = {"pretty", "html:build/reports/tests/test-acceptance", "json:build/reports/tests/test-acceptance/results.json"}, 12 | strict = true 13 | ) 14 | public class TodoUseCaseAcceptanceTest { 15 | } 16 | -------------------------------------------------------------------------------- /src/test-acceptance/resources/todo/adding_todos.feature: -------------------------------------------------------------------------------- 1 | # language: en 2 | 3 | Feature: A user wants to add new todos 4 | 5 | Scenario: A user adds a new todo. 6 | Given an empty list 7 | When user adds a new todo 8 | Then this todo will be added to the list 9 | And this todo will be unchecked 10 | 11 | Scenario: A user tries to add a todo twice. 12 | Given an empty list 13 | When user adds a new todo 14 | And tries to add this todo again 15 | Then this todo will be added twice 16 | 17 | Scenario: A user adds different todos 18 | Given a list with the following todos: 19 | | Read some stuff | 20 | | Get the idea | 21 | When user adds a new todo 22 | Then the list contains 3 todos 23 | 24 | -------------------------------------------------------------------------------- /src/test-acceptance/resources/todo/getting_todo_done.feature: -------------------------------------------------------------------------------- 1 | # language: en 2 | 3 | Feature: A user wants to get todos done. 4 | 5 | Scenario: A user has todos that are still undone. 6 | Given a list with the following todos: 7 | | Read some stuff | 8 | | Get the idea | 9 | | Write some tests | 10 | | Make up docs | 11 | When the todo "Read some stuff" is done 12 | Then the list contains 3 todos -------------------------------------------------------------------------------- /src/test-acceptance/resources/todo/reading_todos.feature: -------------------------------------------------------------------------------- 1 | # language: en 2 | 3 | Feature: A user wants to get an overview over all his todos. 4 | 5 | Scenario: A user has still todos that are still undone. 6 | Given a list with the following todos: 7 | | Read some stuff | 8 | | Get the idea | 9 | | Write some tests | 10 | | Make up docs | 11 | When the user asks for his todos 12 | Then the list contains 4 todos 13 | -------------------------------------------------------------------------------- /src/test-architecture/java/archtest/OnionArchitecture.java: -------------------------------------------------------------------------------- 1 | package archtest; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClasses; 4 | import com.tngtech.archunit.lang.ArchRule; 5 | import com.tngtech.archunit.lang.EvaluationResult; 6 | import com.tngtech.archunit.library.Architectures; 7 | 8 | import java.util.*; 9 | 10 | import static com.tngtech.archunit.library.Architectures.layeredArchitecture; 11 | 12 | public class OnionArchitecture implements ArchRule { 13 | 14 | private enum LayerType { 15 | APPLICATION, 16 | DOMAIN, 17 | INFRASTRUCTURE, 18 | CONFIG 19 | } 20 | 21 | private Map> layerIdentifiers = new HashMap<>(); 22 | 23 | public OnionArchitecture(){ 24 | initLayerIdentifiers(this.layerIdentifiers); 25 | } 26 | 27 | private static void initLayerIdentifiers(final Map> layerIdentifierMap){ 28 | Arrays.asList(LayerType.values()) 29 | .forEach(layerType -> layerIdentifierMap.put(layerType, new ArrayList<>())); 30 | } 31 | 32 | private void addLayerIdentifier(final String packageName, final LayerType layerId){ 33 | this.layerIdentifiers.get(layerId).add(packageName); 34 | } 35 | 36 | public OnionArchitecture applicationPackage(final String packageName){ 37 | addLayerIdentifier(packageName, LayerType.APPLICATION); 38 | return this; 39 | } 40 | 41 | public OnionArchitecture domainPackage(final String packageName){ 42 | addLayerIdentifier(packageName, LayerType.DOMAIN); 43 | return this; 44 | } 45 | 46 | public OnionArchitecture infrastructurePackage(final String packageName){ 47 | addLayerIdentifier(packageName, LayerType.INFRASTRUCTURE); 48 | return this; 49 | } 50 | 51 | public OnionArchitecture configPackage(final String packageName){ 52 | addLayerIdentifier(packageName, LayerType.CONFIG); 53 | return this; 54 | } 55 | 56 | private static String[] asArray(final Collection list){ 57 | return list.toArray(new String[list.size()]); 58 | } 59 | 60 | private static void layer(final Architectures.LayeredArchitecture rule, final Map> layerMap, final LayerType layerType){ 61 | rule.layer(layerType.name()).definedBy(asArray(layerMap.get(layerType))); 62 | } 63 | 64 | private Architectures.LayeredArchitecture layeredArchitectureDelegate() { 65 | final Architectures.LayeredArchitecture layeredArchitecture = layeredArchitecture(); 66 | layer(layeredArchitecture, this.layerIdentifiers, LayerType.APPLICATION); 67 | layer(layeredArchitecture, this.layerIdentifiers, LayerType.DOMAIN); 68 | layer(layeredArchitecture, this.layerIdentifiers, LayerType.INFRASTRUCTURE); 69 | layer(layeredArchitecture, this.layerIdentifiers, LayerType.CONFIG); 70 | layeredArchitecture 71 | .whereLayer(LayerType.DOMAIN.name()).mayOnlyBeAccessedByLayers(LayerType.APPLICATION.name(), LayerType.INFRASTRUCTURE.name(), LayerType.CONFIG.name()) 72 | .whereLayer(LayerType.APPLICATION.name()).mayOnlyBeAccessedByLayers(LayerType.INFRASTRUCTURE.name(), LayerType.CONFIG.name()) 73 | .whereLayer(LayerType.INFRASTRUCTURE.name()).mayOnlyBeAccessedByLayers(LayerType.CONFIG.name()) 74 | .whereLayer(LayerType.CONFIG.name()).mayNotBeAccessedByAnyLayer() 75 | .withOptionalLayers(false) 76 | ; 77 | return layeredArchitecture; 78 | } 79 | 80 | @Override 81 | public ArchRule as(final String newDescription) { 82 | return layeredArchitectureDelegate().as(newDescription); 83 | } 84 | 85 | @Override 86 | public ArchRule because(final String reason) { 87 | return layeredArchitectureDelegate().because(reason); 88 | } 89 | 90 | @Override 91 | public void check(final JavaClasses classes) { 92 | layeredArchitectureDelegate().check(classes); 93 | } 94 | 95 | @Override 96 | public EvaluationResult evaluate(final JavaClasses classes) { 97 | return layeredArchitectureDelegate().evaluate(classes); 98 | } 99 | 100 | @Override 101 | public String getDescription() { 102 | return layeredArchitectureDelegate().getDescription(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test-architecture/java/archtest/PackageType.java: -------------------------------------------------------------------------------- 1 | package archtest; 2 | 3 | public enum PackageType { 4 | 5 | APPLICATION_ROOT("..application"), 6 | APPLICATION_SERVICE("..application.service"), 7 | APPLICATION_USECASE("..application.usecase"), 8 | CONFIG_ROOT("..config"), 9 | DOMAIN_EVENT("..domain.event"), 10 | DOMAIN_EXCEPTION("..domain.exception"), 11 | DOMAIN_MODEL("..domain.model"), 12 | DOMAIN_PORT("..domain.port"), 13 | DOMAIN_ROOT("..domain"), 14 | DOMAIN_SERVICE("..domain.service"), 15 | DOMAIN_SERVICE_IMPL("..domain.service.impl"), 16 | INFRASTRUCTURE_ROOT("..adapter"), 17 | INFRASTRUCTURE_ADAPTER("..adapter"), 18 | 19 | JAVA_ROOT("java"); 20 | 21 | private final String packageName; 22 | 23 | private PackageType(final String packageName) { 24 | this.packageName = packageName; 25 | } 26 | 27 | public String get(){ 28 | return this.packageName; 29 | } 30 | 31 | public String recursive(){ 32 | return String.format("%s..", this.packageName); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test-architecture/java/todo/DddApplicationTest.java: -------------------------------------------------------------------------------- 1 | package todo; 2 | 3 | import archtest.PackageType; 4 | import com.tngtech.archunit.junit.AnalyzeClasses; 5 | import com.tngtech.archunit.junit.ArchTest; 6 | import com.tngtech.archunit.junit.ArchUnitRunner; 7 | import com.tngtech.archunit.lang.ArchRule; 8 | import common.architecture.UseCase; 9 | import io.hschwentner.dddbits.annotation.ApplicationService; 10 | import org.junit.runner.RunWith; 11 | 12 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 13 | 14 | @RunWith(ArchUnitRunner.class) 15 | @AnalyzeClasses(packages = {"todo"}) 16 | public class DddApplicationTest { 17 | 18 | @ArchTest 19 | static final ArchRule useCasesMustBeAnnotatedWithUseCase = 20 | classes() 21 | .that().resideInAnyPackage(PackageType.APPLICATION_USECASE.recursive()) 22 | .should().beAnnotatedWith(UseCase.class); 23 | 24 | @ArchTest 25 | static final ArchRule applicationServicesMustBeAnnotatedWithApplicationService = 26 | classes() 27 | .that().resideInAnyPackage(PackageType.APPLICATION_SERVICE.recursive()) 28 | .should().beAnnotatedWith(ApplicationService.class); 29 | 30 | @ArchTest 31 | static final ArchRule applicationServiceMustImplementUseCases = 32 | classes() 33 | .that().resideInAnyPackage(PackageType.DOMAIN_SERVICE_IMPL.recursive()) 34 | .should().dependOnClassesThat().resideInAnyPackage( 35 | PackageType.DOMAIN_SERVICE.recursive(), 36 | PackageType.JAVA_ROOT.recursive() 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/test-architecture/java/todo/DddArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package todo; 2 | 3 | import archtest.OnionArchitecture; 4 | import archtest.PackageType; 5 | import com.tngtech.archunit.junit.AnalyzeClasses; 6 | import com.tngtech.archunit.junit.ArchIgnore; 7 | import com.tngtech.archunit.junit.ArchTest; 8 | import com.tngtech.archunit.junit.ArchUnitRunner; 9 | import com.tngtech.archunit.lang.ArchRule; 10 | import common.architecture.UseCase; 11 | import io.hschwentner.dddbits.annotation.Aggregate; 12 | import io.hschwentner.dddbits.annotation.AggregateRoot; 13 | import io.hschwentner.dddbits.annotation.DomainEntity; 14 | import io.hschwentner.dddbits.annotation.ValueObject; 15 | import org.junit.runner.RunWith; 16 | 17 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 18 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; 19 | 20 | @RunWith(ArchUnitRunner.class) 21 | @AnalyzeClasses(packages = {"todo"}) 22 | public class DddArchitectureTest { 23 | 24 | @ArchTest 25 | static final ArchRule followsOnionArchitectureStructuredRule = 26 | new OnionArchitecture() 27 | .applicationPackage(PackageType.APPLICATION_ROOT.recursive()) 28 | .domainPackage(PackageType.DOMAIN_ROOT.recursive()) 29 | .infrastructurePackage(PackageType.INFRASTRUCTURE_ROOT.recursive()) 30 | .configPackage(PackageType.CONFIG_ROOT.recursive()); 31 | 32 | @ArchTest 33 | static final ArchRule usecasesShouldBeAnnotatedInterfaces = 34 | classes() 35 | .that().resideInAPackage(PackageType.APPLICATION_ROOT.get()) 36 | .should().beInterfaces() 37 | .andShould().beAnnotatedWith(UseCase.class); 38 | 39 | @ArchTest 40 | static final ArchRule usecaseShouldNotAccessApplicationServices = 41 | noClasses() 42 | .that().resideInAPackage(PackageType.APPLICATION_ROOT.get()) 43 | .should().accessClassesThat().resideInAPackage(PackageType.APPLICATION_SERVICE.get()); 44 | 45 | @ArchIgnore 46 | @ArchTest 47 | static final ArchRule valueObjectMayNotUseEntitiesOrAggregates = 48 | noClasses() 49 | .that().areAnnotatedWith(ValueObject.class) 50 | .should().accessClassesThat().areAnnotatedWith(DomainEntity.class) 51 | .orShould().accessClassesThat().areNotAnnotatedWith(Aggregate.class) 52 | .orShould().accessClassesThat().areNotAnnotatedWith(AggregateRoot.class); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test-architecture/java/todo/DddDomainTest.java: -------------------------------------------------------------------------------- 1 | package todo; 2 | 3 | import archtest.PackageType; 4 | import com.tngtech.archunit.junit.AnalyzeClasses; 5 | import com.tngtech.archunit.junit.ArchTest; 6 | import com.tngtech.archunit.junit.ArchUnitRunner; 7 | import com.tngtech.archunit.lang.ArchRule; 8 | import common.architecture.Port; 9 | import io.hschwentner.dddbits.annotation.*; 10 | import org.junit.runner.RunWith; 11 | 12 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 13 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; 14 | 15 | @RunWith(ArchUnitRunner.class) 16 | @AnalyzeClasses(packages = {"todo"}) 17 | public class DddDomainTest { 18 | 19 | @ArchTest 20 | static final ArchRule domainEventDependsOnlyOnDomainModel = 21 | noClasses() 22 | .that().resideInAPackage(PackageType.DOMAIN_EVENT.recursive()) 23 | .should().accessClassesThat().resideOutsideOfPackages( 24 | PackageType.DOMAIN_EVENT.recursive(), 25 | PackageType.DOMAIN_MODEL.recursive(), 26 | PackageType.JAVA_ROOT.recursive() 27 | ); 28 | 29 | @ArchTest 30 | static final ArchRule domainExceptionsDependsOnlyOnDomainModel = 31 | noClasses() 32 | .that().resideInAPackage(PackageType.DOMAIN_EXCEPTION.recursive()) 33 | .should().accessClassesThat().resideOutsideOfPackages( 34 | PackageType.DOMAIN_EXCEPTION.recursive(), 35 | PackageType.DOMAIN_MODEL.recursive(), 36 | PackageType.JAVA_ROOT.recursive() 37 | ); 38 | 39 | @ArchTest 40 | static final ArchRule domainModelShouldBeAnntotated = 41 | classes() 42 | .that().resideInAPackage(PackageType.DOMAIN_MODEL.recursive()) 43 | .should().beAnnotatedWith(ValueObject.class) 44 | .orShould().beAnnotatedWith(DomainEntity.class) 45 | .orShould().beAnnotatedWith(Aggregate.class) 46 | .orShould().beAnnotatedWith(AggregateRoot.class); 47 | 48 | @ArchTest 49 | static final ArchRule domainModelDependsOnNothing = 50 | noClasses() 51 | .that().resideInAPackage(PackageType.DOMAIN_MODEL.recursive()) 52 | .should().accessClassesThat().resideOutsideOfPackages( 53 | PackageType.DOMAIN_MODEL.recursive(), 54 | PackageType.JAVA_ROOT.recursive() 55 | ); 56 | 57 | @ArchTest 58 | static final ArchRule domainPortShouldBeAnnotatedInterfaces = 59 | classes() 60 | .that().resideInAPackage(PackageType.DOMAIN_PORT.recursive()) 61 | .should().beInterfaces() 62 | .andShould().beAnnotatedWith(Port.class); 63 | 64 | @ArchTest 65 | static final ArchRule domainServiceInterfaceShouldePlacedIn = 66 | classes() 67 | .that().resideInAPackage(PackageType.DOMAIN_SERVICE.get()) 68 | .should().beInterfaces(); 69 | 70 | @ArchTest 71 | static final ArchRule domainServiceImplMustBeAnnotatedWithDomainService = 72 | classes() 73 | .that().resideInAnyPackage(PackageType.DOMAIN_SERVICE_IMPL.recursive()) 74 | .should().beAnnotatedWith(DomainService.class); 75 | 76 | @ArchTest 77 | static final ArchRule domainServiceImplMustImplementDomainServiceInterfaces = 78 | classes() 79 | .that().resideInAnyPackage(PackageType.DOMAIN_SERVICE_IMPL.recursive()) 80 | .should().dependOnClassesThat().resideInAnyPackage( 81 | PackageType.DOMAIN_ROOT.recursive(), 82 | PackageType.JAVA_ROOT.recursive() 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/test-architecture/java/todo/DddInfrastructureTest.java: -------------------------------------------------------------------------------- 1 | package todo; 2 | 3 | import archtest.PackageType; 4 | import com.tngtech.archunit.junit.AnalyzeClasses; 5 | import com.tngtech.archunit.junit.ArchTest; 6 | import com.tngtech.archunit.junit.ArchUnitRunner; 7 | import com.tngtech.archunit.lang.ArchRule; 8 | import common.architecture.Adapter; 9 | import common.architecture.Port; 10 | import org.junit.Ignore; 11 | import org.junit.runner.RunWith; 12 | 13 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 14 | 15 | @Ignore("make a difference between in and out ports/adapters and exclude model classes") //FIXME 16 | @RunWith(ArchUnitRunner.class) 17 | @AnalyzeClasses(packages = {"todo"}) 18 | public class DddInfrastructureTest { 19 | 20 | @ArchTest 21 | static final ArchRule adapterMustBeAnnotatedWithAdapter = 22 | classes() 23 | .that().resideInAnyPackage(PackageType.INFRASTRUCTURE_ADAPTER.recursive()) 24 | .should().beAnnotatedWith(Adapter.class); 25 | 26 | @ArchTest 27 | static final ArchRule adapterMustImplementPort = 28 | classes() 29 | .that().resideInAnyPackage(PackageType.INFRASTRUCTURE_ADAPTER.recursive()) 30 | .should().dependOnClassesThat().resideInAPackage(PackageType.DOMAIN_PORT.recursive()) 31 | .andShould().dependOnClassesThat().areAnnotatedWith(Port.class); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/todo/infrastructure/adapter/db/TodoListInMemoryRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package todo.infrastructure.adapter.db; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import todo.adapter.out.db.TodoListListInMemoryRepository; 6 | import todo.domain.model.TodoList; 7 | import todo.domain.model.UserId; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | public class TodoListInMemoryRepositoryTest { 15 | 16 | private TodoListListInMemoryRepository repsitory; 17 | 18 | @BeforeEach 19 | public void setUp() { 20 | this.repsitory = new TodoListListInMemoryRepository(); 21 | } 22 | 23 | @Test 24 | public void findById() { 25 | // Given 26 | final UserId userId = UserId.create(); 27 | final TodoList todoList = TodoList.create(userId); 28 | this.repsitory.save(todoList); 29 | // When 30 | final Optional opt = this.repsitory.findById(userId); 31 | // Then 32 | assertTrue(opt.isPresent()); 33 | assertEquals(todoList, opt.get()); 34 | } 35 | } --------------------------------------------------------------------------------