├── .gitignore ├── README.md ├── application ├── build.gradle └── src │ └── main │ ├── order │ └── commandhandlers │ │ ├── OrderCommandHandlers.kt │ │ └── commands │ │ ├── AddProductCommand.kt │ │ ├── ChangeProductQuantityCommand.kt │ │ ├── CreateOrderCommand.kt │ │ ├── PayOrderCommand.kt │ │ └── RemoveProductCommand.kt │ └── shipping │ └── eventhandlers │ └── ShipOrderAndNotifyUser.kt ├── build.gradle ├── docs ├── images │ ├── command_side.jpg │ ├── command_side_with_events.jpg │ └── query_side.jpg └── postman_example_requests.json ├── domain ├── build.gradle └── src │ ├── main │ ├── BusinessException.kt │ ├── order │ │ ├── Item.kt │ │ ├── Order.kt │ │ ├── OrderPaid.kt │ │ ├── OrderRepository.kt │ │ ├── Product.kt │ │ ├── customer │ │ │ ├── Address.kt │ │ │ └── Customer.kt │ │ └── payment │ │ │ ├── CreditCard.kt │ │ │ └── PaymentService.kt │ └── shipping │ │ ├── DeliveryProviderService.kt │ │ ├── NotificationService.kt │ │ ├── ShippingLabel.kt │ │ └── ShippingService.kt │ └── test │ └── order │ └── OrderTest.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── infrastructure ├── build.gradle └── src │ └── main │ ├── queries │ ├── OrdersQuery.kt │ └── dtos │ │ ├── FilteredOrderDTO.kt │ │ └── OrderPerUsersDTO.kt │ ├── repositories │ └── OrderRepositoryImpl.kt │ └── services │ ├── EmailNotificationService.kt │ ├── FedExDeliveryService.kt │ └── PaymentServiceImpl.kt ├── settings.gradle └── web ├── build.gradle └── src └── main ├── DDDSampleApplication.kt ├── configuration └── injection │ ├── AMQPRabbitConfiguration.kt │ ├── CommandHandlers.kt │ ├── EventHandlers.kt │ ├── Repositories.kt │ └── Services.kt ├── controllers └── OrdersController.kt ├── models ├── AddProductRequest.kt ├── ChangeProductQuantityRequest.kt ├── CreateOrderRequest.kt └── PayOrderRequest.kt └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /android.tests.dependencies 3 | /confluence/target 4 | /dependencies 5 | /dist 6 | /local 7 | /gh-pages 8 | /ideaSDK 9 | /clionSDK 10 | /android-studio/sdk 11 | out/ 12 | /tmp 13 | workspace.xml 14 | *.versionsBackup 15 | /idea/testData/debugger/tinyApp/classes* 16 | /jps-plugin/testData/kannotator 17 | /ultimate/dependencies 18 | /ultimate/ideaSDK 19 | /ultimate/out 20 | /ultimate/tmp 21 | /js/js.translator/testData/out/ 22 | /js/js.translator/testData/out-min/ 23 | .gradle/ 24 | build/ 25 | !**/src/**/build 26 | !**/test/**/build 27 | *.iml 28 | !**/testData/**/*.iml 29 | .idea/ 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin DDD Sample 2 | 3 | **Kotlin DDD Sample** is a open-source project meant to be used as a start point, or an inspiration, for those who want to build Domain Driven Design applications in Kotlin. The domain model was inspired by [this](https://github.com/mcapanema/ddd-rails-example) repo where we built a sample project using Rails. 4 | 5 | **NOTE:** This is NOT intended to be a definitive solution or a production ready project 6 | 7 | # Technologies/frameworks/tools involved 8 | 9 | - Spring 10 | - Axon Framework 11 | - CommandGateway (Command Handlers) 12 | - EventBus (Event Handlers) 13 | - Gradle 14 | 15 | # Architecture overview 16 | 17 | ## Layers 18 | - **Web**: Spring controllers and actions 19 | - **Application**: Orchestrates the jobs in the domain needed to be done to accomplish a certain "use case" 20 | - **Domain**: Where the business rules resides 21 | - **Infrastructure**: Technologies concerns resides here (database access, sending emails, calling external APIs) 22 | 23 | ## CQRS 24 | 25 | CQRS splits your application (and even the database in some cases) into two different paths: **Commands** and **Queries**. 26 | 27 | ### Command side 28 | 29 | Every operation that can trigger an side effect on the server must pass through the CQRS "command side". I like to put the `Handlers` (commands handlers and events handlers) inside the application layer because their goals are almost the same: orchestrate domain operations (also usually using infrastructure services). 30 | 31 | ![command side](docs/images/command_side_with_events.jpg) 32 | 33 | ### Query side 34 | 35 | Pretty straight forward, the controller receives the request, calls the related query repo and returns a DTO (defined on infrastructure layer itself). 36 | 37 | ![query side](docs/images/query_side.jpg) 38 | 39 | # The domain (problem space) 40 | 41 | This project is based on a didactic domain that basically consists in maintaining an Order (adding and removing items in it). The operations supported by the application are: 42 | 43 | * Create an order 44 | * Add products ta a given order 45 | * Change product quantity 46 | * Remove product 47 | * Pay the order (this operation fires an Event for the shipping bounded context) 48 | * Shipping (side effect of the above event): ships product and notify user 49 | 50 | Pretty simple, right? 51 | 52 | # Setup 53 | 54 | **Linux/MacOS:** 55 | 56 | ``` 57 | ./gradlew build 58 | ``` 59 | 60 | **Windows:** 61 | 62 | ``` 63 | gradlew.bat build 64 | ``` 65 | 66 | ### RabbitMQ setup 67 | 68 | There is a file named `AMQPRabbitConfiguration` in this repo (located [here](https://github.com/fabriciorissetto/kotlin-ddd-sample/blob/master/web/src/main/configuration/injection/AMQPRabbitConfiguration.kt)) where the configuration needed by axon to integrate with RabbitMQ (to send end receive persistent messages) is stored. To use that, just remove the comments. 69 | 70 | You need a running rabbit, you can start one in a docker container using the following commands: 71 | 72 | ```bash 73 | docker pull rabbitmq 74 | docker run -d --hostname my-rabbit --name some-rabbit rabbitmq:3-management 75 | ``` 76 | 77 | You can access the rabbit UI by this url: [http://172.17.0.2:15672](http://172.17.0.2:15672). 78 | 79 | * **User**: guest 80 | * **Password**: guest 81 | 82 | That's it. You don't need to do anything else, the setup in the `AMQPRabbitConfiguration` class will create the necessary queue and exchange in Rabbit and also configure axon accordingly. Note that if you customize something in your rabbit server you need to adjust the `application.properties` file (here we are using the default ports, ips, etc). 83 | 84 | This both dependencies are used just for Rabbit: 85 | * `org.springframework.boot:spring-boot-starter-amqp`: enables AMQP in Spring Boot 86 | * `org.axonframework:axon-amqp`: configures some beans for axon to integrate with `SpringAMQPMessageSource` class from the above dependency 87 | 88 | If you don't want o use an AMQP you can remove this dependencies from the web project gradle's file. 89 | 90 | # Tests 91 | 92 | ``` 93 | ./gradlew test 94 | ``` 95 | 96 | ### Postman requests 97 | 98 | You can trigger all the operations of this project using the requests inside [this json](https://github.com/fabriciorissetto/kotlin-ddd-sample/blob/master/docs/postman_example_requests.json) (just import it on your local postman). 99 | 100 | # Backlog 101 | - [x] Implement Unit Tests examples (Domain layer) 102 | - [ ] Implement Integrated Tests examples (Web layer) 103 | - [ ] Include docker container with JDK and gradle configured 104 | - [ ] Configure Swagger and Swagger UI 105 | - [ ] Include a Event Sourced bounded context or Aggregate 106 | - [ ] Domain Notifications instead of raising exceptions 107 | - [ ] Implement concrete repositories with JPA (the current implementations just returns fake instances) 108 | - [ ] Configure JPMS (java 9 modules) 109 | 110 | Contributions are welcome! :heartbeat: 111 | -------------------------------------------------------------------------------- /application/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(":domain") 3 | } -------------------------------------------------------------------------------- /application/src/main/order/commandhandlers/OrderCommandHandlers.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.application.order.commandhandlers 2 | 3 | import kotlinddd.application.order.commandhandlers.commands.* 4 | import kotlinddd.domain.order.Order 5 | import kotlinddd.domain.order.OrderRepository 6 | import org.axonframework.commandhandling.CommandHandler 7 | import kotlinddd.domain.order.payment.PaymentService 8 | import org.axonframework.eventhandling.EventBus 9 | import java.util.UUID 10 | 11 | open class OrderCommandHandlers(private val repository: OrderRepository, 12 | private val paymentService: PaymentService, 13 | private val eventBus: EventBus) { 14 | @CommandHandler 15 | fun createOrder(command: CreateOrderCommand): UUID { 16 | val orderId = UUID.randomUUID() 17 | val customer = repository.findCustomerById(command.customerId) 18 | 19 | val order = Order(orderId, customer) 20 | repository.save(order) 21 | 22 | return orderId 23 | } 24 | 25 | @CommandHandler 26 | fun addProduct(command: AddProductCommand) { 27 | val order = repository.findById(command.orderId) 28 | val product = repository.findProductById(command.productId) 29 | 30 | order.addProduct(product, command.quantity) 31 | 32 | repository.save(order) 33 | } 34 | 35 | @CommandHandler 36 | fun changeProductQuantity(command: ChangeProductQuantityCommand) { 37 | val order = repository.findById(command.orderId) 38 | val product = repository.findProductById(command.productId) 39 | 40 | order.changeProductQuantity(product, command.quantity) 41 | 42 | repository.save(order) 43 | } 44 | 45 | @CommandHandler 46 | fun removeProduct(command: RemoveProductCommand) { 47 | val order = repository.findById(command.orderId) 48 | val product = repository.findProductById(command.productId) 49 | 50 | order.removeProduct(product) 51 | 52 | repository.save(order) 53 | } 54 | 55 | @CommandHandler 56 | fun payOrder(command: PayOrderCommand) { 57 | val order = repository.findById(command.orderId) 58 | 59 | order.pay(command.creditCard, paymentService, eventBus) //TODO provide global access to eventBus 60 | 61 | repository.save(order) 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /application/src/main/order/commandhandlers/commands/AddProductCommand.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.application.order.commandhandlers.commands 2 | 3 | import java.util.UUID 4 | 5 | data class AddProductCommand(val orderId: UUID, val productId: UUID, val quantity: Int) -------------------------------------------------------------------------------- /application/src/main/order/commandhandlers/commands/ChangeProductQuantityCommand.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.application.order.commandhandlers.commands 2 | 3 | import java.util.UUID 4 | 5 | data class ChangeProductQuantityCommand(val orderId: UUID, val productId: UUID, val quantity: Int) -------------------------------------------------------------------------------- /application/src/main/order/commandhandlers/commands/CreateOrderCommand.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.application.order.commandhandlers.commands 2 | 3 | import java.util.UUID 4 | 5 | data class CreateOrderCommand(val customerId: UUID) -------------------------------------------------------------------------------- /application/src/main/order/commandhandlers/commands/PayOrderCommand.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.application.order.commandhandlers.commands 2 | 3 | import kotlinddd.domain.order.payment.CreditCard 4 | import java.util.UUID 5 | 6 | data class PayOrderCommand(val orderId: UUID, val creditCard: CreditCard) -------------------------------------------------------------------------------- /application/src/main/order/commandhandlers/commands/RemoveProductCommand.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.application.order.commandhandlers.commands 2 | 3 | import java.util.UUID 4 | 5 | data class RemoveProductCommand(val orderId: UUID, val productId: UUID) -------------------------------------------------------------------------------- /application/src/main/shipping/eventhandlers/ShipOrderAndNotifyUser.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.application.shipping.eventhandlers 2 | 3 | import kotlinddd.domain.order.OrderPaid 4 | import kotlinddd.domain.order.OrderRepository 5 | import kotlinddd.domain.shipping.ShippingService 6 | import org.axonframework.eventhandling.EventHandler 7 | 8 | open class ShipOrderAndNotifyUser(private val orderRepository: OrderRepository, 9 | private val shippingService: ShippingService) { 10 | @EventHandler 11 | fun handle(event: OrderPaid) { 12 | val order = orderRepository.findById(event.orderId) 13 | shippingService.shipOrder(order) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.3.31' 4 | } 5 | repositories { 6 | maven { 7 | url "https://plugins.gradle.org/m2/" 8 | } 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") 13 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") 14 | } 15 | } 16 | 17 | group = 'kotlinddd' 18 | version = '1.0-SNAPSHOT' 19 | ext.axonVersion = '3.3' 20 | 21 | allprojects { 22 | repositories { 23 | jcenter() 24 | } 25 | 26 | apply plugin: 'kotlin' 27 | 28 | compileKotlin { 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | } 32 | } 33 | 34 | compileTestKotlin { 35 | kotlinOptions { 36 | jvmTarget = "1.8" 37 | } 38 | } 39 | 40 | dependencies { 41 | compile "org.jetbrains.kotlin:kotlin-stdlib" 42 | compile "org.axonframework:axon-core:$axonVersion" 43 | testCompile "org.axonframework:axon-test:$axonVersion" 44 | testCompile "junit:junit:4.12" 45 | testImplementation "org.mockito:mockito-core:2.19.0" 46 | testImplementation "org.amshove.kluent:kluent:1.40" 47 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0" 48 | } 49 | } 50 | 51 | subprojects { 52 | sourceSets { 53 | main.kotlin.srcDirs += 'src/main' 54 | test.kotlin.srcDirs += 'src/test' 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/images/command_side.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creditas/kotlin-ddd-sample/5a1835a5f790705fb42deef3a675104f4fe3b535/docs/images/command_side.jpg -------------------------------------------------------------------------------- /docs/images/command_side_with_events.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creditas/kotlin-ddd-sample/5a1835a5f790705fb42deef3a675104f4fe3b535/docs/images/command_side_with_events.jpg -------------------------------------------------------------------------------- /docs/images/query_side.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creditas/kotlin-ddd-sample/5a1835a5f790705fb42deef3a675104f4fe3b535/docs/images/query_side.jpg -------------------------------------------------------------------------------- /docs/postman_example_requests.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "b6c3de12-426c-d39a-c242-8a6d3eed22c8", 3 | "name": "Kotlin DDD Sample", 4 | "description": null, 5 | "order": [ 6 | "e925dd47-9577-d3c5-7c2a-23cbb461a1dd", 7 | "239a91b2-1988-617a-074f-97db653c40ad", 8 | "9800bb20-10b9-64dd-f4af-609e2437291f", 9 | "4f0c1c4d-f2d6-a3a6-f2be-b5a6b20a2310", 10 | "987f7fba-f421-ad3f-7f83-7db50a946027" 11 | ], 12 | "folders": [], 13 | "folders_order": [], 14 | "timestamp": 0, 15 | "owner": "363237", 16 | "public": false, 17 | "requests": [ 18 | { 19 | "id": "239a91b2-1988-617a-074f-97db653c40ad", 20 | "headers": "Content-Type: application/json\n", 21 | "headerData": [ 22 | { 23 | "key": "Content-Type", 24 | "value": "application/json", 25 | "description": "", 26 | "enabled": true 27 | } 28 | ], 29 | "url": "http://localhost:3000/orders/24/products", 30 | "queryParams": [], 31 | "pathVariables": {}, 32 | "pathVariableData": [], 33 | "preRequestScript": null, 34 | "method": "PATCH", 35 | "collectionId": "b6c3de12-426c-d39a-c242-8a6d3eed22c8", 36 | "data": [], 37 | "dataMode": "raw", 38 | "name": "Add Product", 39 | "description": "", 40 | "descriptionFormat": "html", 41 | "time": 1525482047451, 42 | "version": 2, 43 | "responses": [], 44 | "tests": null, 45 | "currentHelper": "normal", 46 | "helperAttributes": {}, 47 | "rawModeData": "{\n \"product_id\": 2,\n \"quantity\": 10\n}" 48 | }, 49 | { 50 | "id": "4f0c1c4d-f2d6-a3a6-f2be-b5a6b20a2310", 51 | "headers": "", 52 | "headerData": [], 53 | "url": "http://localhost:3000/orders/24/products/2", 54 | "queryParams": [], 55 | "pathVariables": {}, 56 | "pathVariableData": [], 57 | "preRequestScript": null, 58 | "method": "DELETE", 59 | "collectionId": "b6c3de12-426c-d39a-c242-8a6d3eed22c8", 60 | "data": null, 61 | "dataMode": "params", 62 | "name": "Remove Product", 63 | "description": "", 64 | "descriptionFormat": "html", 65 | "time": 1525481956607, 66 | "version": 2, 67 | "responses": [], 68 | "tests": null, 69 | "currentHelper": "normal", 70 | "helperAttributes": {}, 71 | "isFromCollection": true, 72 | "collectionRequestId": "11a82460-a87d-71cf-6c50-cb4df1b2ede7" 73 | }, 74 | { 75 | "id": "9800bb20-10b9-64dd-f4af-609e2437291f", 76 | "headers": "Content-Type: application/json\n", 77 | "headerData": [ 78 | { 79 | "key": "Content-Type", 80 | "value": "application/json", 81 | "description": "", 82 | "enabled": true 83 | } 84 | ], 85 | "url": "http://localhost:3000/orders/24/products/2", 86 | "queryParams": [], 87 | "preRequestScript": null, 88 | "pathVariables": {}, 89 | "pathVariableData": [], 90 | "method": "PATCH", 91 | "data": [], 92 | "dataMode": "raw", 93 | "tests": null, 94 | "currentHelper": "normal", 95 | "helperAttributes": {}, 96 | "time": 1525482324890, 97 | "name": "Change Product Quantity", 98 | "description": "", 99 | "collectionId": "b6c3de12-426c-d39a-c242-8a6d3eed22c8", 100 | "responses": [], 101 | "rawModeData": "{\n\t\"quantity\": 6\n}\n" 102 | }, 103 | { 104 | "id": "987f7fba-f421-ad3f-7f83-7db50a946027", 105 | "headers": "Content-Type: application/json\n", 106 | "headerData": [ 107 | { 108 | "key": "Content-Type", 109 | "value": "application/json", 110 | "description": "", 111 | "enabled": true 112 | } 113 | ], 114 | "url": "http://localhost:8080/orders/5c9d6e6e-f3c6-430a-b55e-3bfc76a75db8", 115 | "queryParams": [], 116 | "preRequestScript": null, 117 | "pathVariables": {}, 118 | "pathVariableData": [], 119 | "method": "PATCH", 120 | "data": [], 121 | "dataMode": "raw", 122 | "version": 2, 123 | "tests": null, 124 | "currentHelper": "normal", 125 | "helperAttributes": {}, 126 | "time": 1531228983545, 127 | "name": "Pay Order", 128 | "description": "", 129 | "collectionId": "b6c3de12-426c-d39a-c242-8a6d3eed22c8", 130 | "responses": [], 131 | "rawModeData": "{\n \"cardName\": \"a\",\n \"cardNumber\": \"b\",\n \"expirationDate\": \"2020-10-10\",\n \"verificationCode\": \"c\"\n}" 132 | }, 133 | { 134 | "id": "e925dd47-9577-d3c5-7c2a-23cbb461a1dd", 135 | "headers": "Content-Type: application/json\n", 136 | "headerData": [ 137 | { 138 | "key": "Content-Type", 139 | "value": "application/json", 140 | "description": "", 141 | "enabled": true 142 | } 143 | ], 144 | "url": "http://localhost:8080/orders", 145 | "queryParams": [], 146 | "preRequestScript": null, 147 | "pathVariables": {}, 148 | "pathVariableData": [], 149 | "method": "POST", 150 | "data": [], 151 | "dataMode": "raw", 152 | "version": 2, 153 | "tests": null, 154 | "currentHelper": "normal", 155 | "helperAttributes": {}, 156 | "time": 1530833588608, 157 | "name": "Create Order", 158 | "description": "", 159 | "collectionId": "b6c3de12-426c-d39a-c242-8a6d3eed22c8", 160 | "responses": [], 161 | "isFromCollection": true, 162 | "collectionRequestId": "438453b3-118e-7faa-0490-ed01ee684ad5", 163 | "rawModeData": "{\n \"customerId\": \"f4587fa6-4080-49aa-be6a-ba8c2c15eda0\"\n}" 164 | } 165 | ] 166 | } -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile "org.javamoney:moneta:1.2.1" 3 | } -------------------------------------------------------------------------------- /domain/src/main/BusinessException.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain 2 | 3 | data class BusinessException(override var message: String): Exception(message) -------------------------------------------------------------------------------- /domain/src/main/order/Item.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order 2 | 3 | import kotlinddd.domain.BusinessException 4 | 5 | class Item(val product: Product, private var quantity: Int) { 6 | init { 7 | validateQuantity(quantity) 8 | } 9 | 10 | fun changeQuantity(quantity: Int) { 11 | validateQuantity(quantity) 12 | this.quantity = quantity 13 | } 14 | 15 | fun validateQuantity(quantity: Int) { 16 | if (quantity <= 0) 17 | throw BusinessException("Quantity must be greater than zero") 18 | } 19 | } -------------------------------------------------------------------------------- /domain/src/main/order/Order.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order 2 | 3 | import kotlinddd.domain.BusinessException 4 | import kotlinddd.domain.order.payment.CreditCard 5 | import kotlinddd.domain.order.payment.PaymentService 6 | import kotlinddd.domain.order.customer.Customer 7 | import org.axonframework.eventhandling.EventBus 8 | import org.axonframework.eventhandling.GenericEventMessage 9 | import java.util.UUID 10 | 11 | class Order(val id: UUID, val customer: Customer) { 12 | private val items = mutableListOf() 13 | var paid: Boolean = false 14 | private set 15 | 16 | fun addProduct(product: Product, quantity: Int) { 17 | if (items.any { it.product == product }) 18 | throw BusinessException("Product already exists!") 19 | 20 | var item = Item(product, quantity) 21 | items.add(item) 22 | } 23 | 24 | fun changeProductQuantity(product: Product, quantity: Int) { 25 | validateIfProductIsOnList(product) 26 | 27 | var item = items.first { it.product == product } 28 | item.changeQuantity(quantity) 29 | } 30 | 31 | fun removeProduct(product: Product) { 32 | validateIfProductIsOnList(product) 33 | 34 | items.removeAll { it.product == product } 35 | } 36 | 37 | fun pay(creditCard: CreditCard, paymentService: PaymentService, eventBus: EventBus) { 38 | if (this.paid) 39 | throw BusinessException("Order already paid!") 40 | 41 | val debitedWithSuccess = paymentService.debitValueByCreditCard(creditCard) 42 | if (debitedWithSuccess) { 43 | this.paid = true 44 | eventBus.publish(GenericEventMessage.asEventMessage(OrderPaid(this.id))) //TODO improve this by putting some helpers in a aggregate base class and may creating an DomainEvent base class 45 | } else { 46 | throw BusinessException("The amount could not be debited from this credit card") 47 | } 48 | } 49 | 50 | fun items() = items.toList() 51 | 52 | private fun validateIfProductIsOnList(product: Product) { 53 | var isOnList = items.any { it.product == product } 54 | if (!isOnList) 55 | throw BusinessException("The product isn't included in this order") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /domain/src/main/order/OrderPaid.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order 2 | 3 | import java.util.UUID 4 | 5 | data class OrderPaid(val orderId: UUID) -------------------------------------------------------------------------------- /domain/src/main/order/OrderRepository.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order 2 | 3 | import kotlinddd.domain.order.customer.Customer 4 | import java.util.UUID 5 | 6 | interface OrderRepository { 7 | fun findById(id: UUID) : Order 8 | fun findProductById(productId: UUID): Product 9 | fun findCustomerById(customerId: UUID): Customer 10 | fun save(order: Order) 11 | } -------------------------------------------------------------------------------- /domain/src/main/order/Product.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order 2 | 3 | import javax.money.MonetaryAmount 4 | import java.util.UUID 5 | 6 | data class Product(val id: UUID, val description: String, val value: MonetaryAmount) 7 | -------------------------------------------------------------------------------- /domain/src/main/order/customer/Address.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order.customer 2 | 3 | data class Address(val street: String, val number: Int, val state: String, val country: String) -------------------------------------------------------------------------------- /domain/src/main/order/customer/Customer.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order.customer 2 | 3 | import java.util.UUID 4 | 5 | data class Customer(val id: UUID, val name: String, val address: Address) 6 | -------------------------------------------------------------------------------- /domain/src/main/order/payment/CreditCard.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order.payment 2 | 3 | import java.util.Date 4 | 5 | data class CreditCard(val fullName: String, val cardNumber: String, val expirationDate: Date, val verificationCode: String) -------------------------------------------------------------------------------- /domain/src/main/order/payment/PaymentService.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order.payment 2 | 3 | interface PaymentService { 4 | fun debitValueByCreditCard(creditCard: CreditCard) : Boolean 5 | } -------------------------------------------------------------------------------- /domain/src/main/shipping/DeliveryProviderService.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.shipping 2 | 3 | import kotlinddd.domain.order.Order 4 | 5 | interface DeliveryProviderService { 6 | fun requestFastDelivery(order: Order) : ShippingLabel 7 | fun requestStandardDelivery(order: Order) : ShippingLabel 8 | } -------------------------------------------------------------------------------- /domain/src/main/shipping/NotificationService.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.shipping 2 | 3 | import kotlinddd.domain.order.customer.Customer 4 | import java.util.UUID 5 | 6 | interface NotificationService { 7 | fun notifyCustomerOrderShipped(customer: Customer, id: UUID, shippingLabel: ShippingLabel) 8 | } -------------------------------------------------------------------------------- /domain/src/main/shipping/ShippingLabel.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.shipping 2 | 3 | import java.util.Date 4 | 5 | data class ShippingLabel(val number: String, val estimatedArrival: Date) -------------------------------------------------------------------------------- /domain/src/main/shipping/ShippingService.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.shipping 2 | 3 | import kotlinddd.domain.order.Order 4 | 5 | class ShippingService(private val notificationService: NotificationService, 6 | private val deliveryProviderService: DeliveryProviderService) { 7 | 8 | fun shipOrder(order: Order) { 9 | val shippingLabel = if (availableForUltraFastShipping(order)) 10 | deliveryProviderService.requestFastDelivery(order) 11 | else 12 | deliveryProviderService.requestStandardDelivery(order) 13 | 14 | notificationService.notifyCustomerOrderShipped(order.customer, order.id, shippingLabel) 15 | } 16 | 17 | private fun availableForUltraFastShipping(order: Order) : Boolean { 18 | val availableStates = listOf("EU", "JP", "BR", "FR") 19 | 20 | return availableStates.contains(order.customer.address.state) 21 | } 22 | } -------------------------------------------------------------------------------- /domain/src/test/order/OrderTest.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.domain.order 2 | 3 | import kotlinddd.domain.order.customer.* 4 | import org.amshove.kluent.* 5 | import org.javamoney.moneta.Money 6 | import org.junit.Test 7 | import java.util.* 8 | import javax.money.* 9 | 10 | class OrderTest { 11 | private val address = Address("Foo", 123, "LA", "US") 12 | private val customer = Customer(UUID.randomUUID(), "Jhon", address) 13 | 14 | @Test 15 | fun `Must to add a product to an order successfully`() { 16 | val order = Order(UUID.randomUUID(), customer) 17 | val product = Product(UUID.randomUUID(), "Book", Money.of(30.50, Monetary.getCurrency("USD"))) 18 | 19 | order.addProduct(product, 10) 20 | 21 | order.items().count() shouldBe 1 22 | } 23 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Creditas/kotlin-ddd-sample/5a1835a5f790705fb42deef3a675104f4fe3b535/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 03 14:37:48 BRT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /infrastructure/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(":domain") 3 | } -------------------------------------------------------------------------------- /infrastructure/src/main/queries/OrdersQuery.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.infrastructure.queries 2 | 3 | import kotlinddd.infrastructure.queries.dtos.ItemDTO 4 | import kotlinddd.infrastructure.queries.dtos.OrderDTO 5 | import kotlinddd.infrastructure.queries.dtos.OrderPerUsersDTO 6 | import java.util.* 7 | 8 | class OrdersQuery { 9 | companion object { 10 | fun findOrderById(id: UUID): OrderDTO { 11 | return OrderDTO(id, listOf(ItemDTO("abc", 1, 2.0))) 12 | } 13 | 14 | fun findOrderPerUsers(): List { 15 | return listOf(OrderPerUsersDTO("john", 10), 16 | OrderPerUsersDTO("ana", 11), 17 | OrderPerUsersDTO("asd", 12), 18 | OrderPerUsersDTO("aba", 13), 19 | OrderPerUsersDTO("bla", 14)) 20 | } 21 | 22 | fun findLastOrders(): List { 23 | // Example returning a list of anonymous classes (to avoid the creation of a DTO class) 24 | return listOf(object { 25 | val id = UUID.randomUUID() 26 | val customer = "jhon" 27 | }) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /infrastructure/src/main/queries/dtos/FilteredOrderDTO.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.infrastructure.queries.dtos 2 | 3 | import java.util.* 4 | 5 | data class OrderDTO(val id: UUID, val items: List) 6 | 7 | data class ItemDTO(val description: String, val quantity: Int, val value: Double) 8 | 9 | -------------------------------------------------------------------------------- /infrastructure/src/main/queries/dtos/OrderPerUsersDTO.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.infrastructure.queries.dtos 2 | 3 | class OrderPerUsersDTO(val customer: String, val quantity: Int) 4 | 5 | -------------------------------------------------------------------------------- /infrastructure/src/main/repositories/OrderRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.infrastructure.repositories 2 | 3 | import kotlinddd.domain.order.Order 4 | import kotlinddd.domain.order.OrderRepository 5 | import kotlinddd.domain.order.Product 6 | import kotlinddd.domain.order.customer.Address 7 | import kotlinddd.domain.order.customer.Customer 8 | import org.javamoney.moneta.Money 9 | import java.util.UUID 10 | import javax.money.Monetary 11 | 12 | class OrderRepositoryImpl : OrderRepository { 13 | private val fakeCustomer = Customer(UUID.randomUUID(), "John Doe", Address("a",1, "c", "d")) 14 | 15 | override fun findById(id: UUID): Order { 16 | return Order(id = id, 17 | customer = fakeCustomer) 18 | } 19 | 20 | override fun findProductById(productId: UUID): Product { 21 | return Product(productId, 22 | "Keyboard", 23 | Money.of(19.90, Monetary.getCurrency("USD"))) 24 | } 25 | 26 | override fun findCustomerById(customerId: UUID): Customer { 27 | return fakeCustomer 28 | } 29 | 30 | override fun save(order: Order) { 31 | //TODO 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /infrastructure/src/main/services/EmailNotificationService.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.infrastructure.services 2 | 3 | import kotlinddd.domain.order.customer.Customer 4 | import kotlinddd.domain.shipping.NotificationService 5 | import kotlinddd.domain.shipping.ShippingLabel 6 | import java.util.* 7 | 8 | class EmailNotificationService : NotificationService { 9 | override fun notifyCustomerOrderShipped(customer: Customer, id: UUID, shippingLabel: ShippingLabel) { 10 | // populate and send mail ... 11 | } 12 | } -------------------------------------------------------------------------------- /infrastructure/src/main/services/FedExDeliveryService.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.infrastructure.services 2 | 3 | import kotlinddd.domain.order.Order 4 | import kotlinddd.domain.shipping.DeliveryProviderService 5 | import kotlinddd.domain.shipping.ShippingLabel 6 | import java.util.Date 7 | 8 | class FedExDeliveryService : DeliveryProviderService { 9 | override fun requestFastDelivery(order: Order): ShippingLabel { 10 | // call FedEx service... 11 | 12 | return ShippingLabel("ABCD-123456", Date()) 13 | } 14 | 15 | override fun requestStandardDelivery(order: Order): ShippingLabel { 16 | // call FedEx service... 17 | 18 | return ShippingLabel("EFGH-456789", Date()) 19 | } 20 | } -------------------------------------------------------------------------------- /infrastructure/src/main/services/PaymentServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.infrastructure.services 2 | 3 | import kotlinddd.domain.order.payment.CreditCard 4 | import kotlinddd.domain.order.payment.PaymentService 5 | 6 | class PaymentServiceImpl : PaymentService { 7 | override fun debitValueByCreditCard(creditCard: CreditCard): Boolean { 8 | return true 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kotlinddd' 2 | 3 | include 'web', 'application', 'domain', 'infrastructure' -------------------------------------------------------------------------------- /web/build.gradle: -------------------------------------------------------------------------------- 1 | sourceCompatibility = 1.10 2 | 3 | buildscript { 4 | ext.spring_boot_version = '2.0.3.RELEASE' 5 | 6 | repositories { 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version" 11 | } 12 | } 13 | 14 | apply plugin: 'kotlin-spring' 15 | apply plugin: 'org.springframework.boot' 16 | apply plugin: 'io.spring.dependency-management' 17 | 18 | dependencies { 19 | implementation project(":application") 20 | implementation project(":domain") 21 | implementation project(":infrastructure") 22 | 23 | implementation "org.jetbrains.kotlin:kotlin-reflect" 24 | implementation "org.springframework.boot:spring-boot-starter" 25 | implementation "org.springframework.boot:spring-boot-starter-web" 26 | implementation "org.springframework.boot:spring-boot-starter-amqp" 27 | implementation "org.springframework.amqp:spring-rabbit:$spring_boot_version" 28 | testImplementation "org.springframework.boot:spring-boot-starter-test" 29 | 30 | implementation "org.axonframework:axon-spring:$axonVersion" 31 | implementation "org.axonframework:axon-spring-boot-autoconfigure:$axonVersion" 32 | implementation "org.axonframework:axon-amqp:$axonVersion" 33 | } 34 | -------------------------------------------------------------------------------- /web/src/main/DDDSampleApplication.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web 2 | 3 | import org.springframework.boot.* 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | 6 | @SpringBootApplication 7 | class DDDSampleApplication 8 | 9 | fun main(args: Array) { 10 | runApplication() 11 | } -------------------------------------------------------------------------------- /web/src/main/configuration/injection/AMQPRabbitConfiguration.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.configuration.injection 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.context.annotation.Bean 5 | import org.springframework.context.annotation.Configuration 6 | import org.axonframework.amqp.eventhandling.spring.SpringAMQPMessageSource 7 | import com.rabbitmq.client.Channel 8 | import org.axonframework.serialization.Serializer 9 | import org.springframework.amqp.rabbit.annotation.RabbitListener 10 | import org.axonframework.config.EventHandlingConfiguration 11 | import org.axonframework.config.DefaultConfigurer 12 | import org.axonframework.config.EventProcessingConfiguration 13 | import org.springframework.amqp.core.* 14 | 15 | //@Configuration 16 | //class AMQPRabbitConfiguration { 17 | // @Autowired 18 | // lateinit var springAMQPMessageSource: SpringAMQPMessageSource 19 | // 20 | // // Configuring processors to use the below message source 21 | // @Autowired 22 | // fun configure(configurer: DefaultConfigurer, eventProcessingConfiguration: EventProcessingConfiguration, eventHandlingConfiguration: EventHandlingConfiguration, rabbitMessageSource: SpringAMQPMessageSource) { 23 | // //TODO: find a way to make the springAMQPMessageSource the default source for all processors 24 | // eventProcessingConfiguration.registerSubscribingEventProcessor("kotlinddd.application.shipping.eventhandlers") { _ -> springAMQPMessageSource } 25 | // } 26 | // 27 | // // Creating a MessageSource (that reads from rabbit) and sends received messages to axon 28 | // @Bean 29 | // fun myRabbitMessageSource(serializer: Serializer): SpringAMQPMessageSource { 30 | // return object : SpringAMQPMessageSource(serializer) { 31 | // @RabbitListener(queues = ["DomainEvents"]) 32 | // @Throws(Exception::class) 33 | // override fun onMessage(message: Message, channel: Channel) { 34 | // super.onMessage(message, channel) 35 | // } 36 | // } 37 | // } 38 | // 39 | // // RABBIT SETUP 40 | // // Creating an exchange 41 | // @Bean 42 | // fun fanout(): FanoutExchange { 43 | // return FanoutExchange("Axon.EventBus") 44 | // } 45 | // 46 | // // Creating the queue 47 | // @Bean 48 | // fun domainEventsQueue(): Queue { 49 | // return Queue("DomainEvents", true, false, false) 50 | // } 51 | // 52 | // // Creating a binding between the queue and the exchange 53 | // @Bean 54 | // fun bindingAxon(fanout: FanoutExchange, domainEventsQueue: Queue): Binding { 55 | // return BindingBuilder.bind(domainEventsQueue).to(fanout) 56 | // } 57 | //} 58 | -------------------------------------------------------------------------------- /web/src/main/configuration/injection/CommandHandlers.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.configuration.injection 2 | 3 | import kotlinddd.application.order.commandhandlers.OrderCommandHandlers 4 | import kotlinddd.domain.order.OrderRepository 5 | import kotlinddd.domain.order.payment.PaymentService 6 | import org.axonframework.eventhandling.EventBus 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.context.annotation.Configuration 10 | 11 | @Configuration 12 | class CommandHandlers { 13 | @Autowired 14 | lateinit var orderRepository: OrderRepository 15 | 16 | @Autowired 17 | lateinit var paymentService: PaymentService 18 | 19 | @Autowired 20 | lateinit var eventBus: EventBus 21 | 22 | @Bean 23 | fun getOrderCommandHandler(): OrderCommandHandlers { 24 | return OrderCommandHandlers(orderRepository, paymentService, eventBus) 25 | } 26 | } -------------------------------------------------------------------------------- /web/src/main/configuration/injection/EventHandlers.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.configuration.injection 2 | 3 | import kotlinddd.application.shipping.eventhandlers.ShipOrderAndNotifyUser 4 | import kotlinddd.domain.order.OrderRepository 5 | import kotlinddd.domain.shipping.ShippingService 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | 10 | @Configuration 11 | class EventHandlers { 12 | @Autowired 13 | lateinit var orderRepository: OrderRepository 14 | 15 | @Autowired 16 | lateinit var shippingService: ShippingService 17 | 18 | @Bean 19 | fun getShipPaidProductAndNotifyUserEventHandler(): ShipOrderAndNotifyUser { 20 | return ShipOrderAndNotifyUser(orderRepository, shippingService) 21 | } 22 | } -------------------------------------------------------------------------------- /web/src/main/configuration/injection/Repositories.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.configuration.injection 2 | 3 | import kotlinddd.domain.order.OrderRepository 4 | import kotlinddd.infrastructure.repositories.OrderRepositoryImpl 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | 8 | @Configuration 9 | class Repositories { 10 | @Bean 11 | fun getOrderRepository() : OrderRepository { 12 | return OrderRepositoryImpl() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web/src/main/configuration/injection/Services.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.configuration.injection 2 | 3 | import kotlinddd.domain.shipping.ShippingService 4 | import kotlinddd.infrastructure.services.EmailNotificationService 5 | import kotlinddd.infrastructure.services.FedExDeliveryService 6 | import kotlinddd.infrastructure.services.PaymentServiceImpl 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | 10 | @Configuration 11 | class Services { 12 | @Bean 13 | fun getPaymentService(): PaymentServiceImpl { 14 | return PaymentServiceImpl() 15 | } 16 | 17 | @Bean 18 | fun getFedExDeliveryService(): FedExDeliveryService { 19 | return FedExDeliveryService() 20 | } 21 | 22 | @Bean 23 | fun getEmailNotificationService(): EmailNotificationService { 24 | return EmailNotificationService() 25 | } 26 | 27 | @Bean 28 | fun getShippingService(): ShippingService { 29 | return ShippingService(getEmailNotificationService(), getFedExDeliveryService()) 30 | } 31 | } -------------------------------------------------------------------------------- /web/src/main/controllers/OrdersController.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.controllers 2 | 3 | import kotlinddd.application.order.commandhandlers.commands.* 4 | import kotlinddd.domain.BusinessException 5 | import kotlinddd.domain.order.payment.CreditCard 6 | import kotlinddd.infrastructure.queries.OrdersQuery 7 | import kotlinddd.infrastructure.queries.dtos.OrderDTO 8 | import kotlinddd.infrastructure.queries.dtos.OrderPerUsersDTO 9 | import kotlinddd.web.models.* 10 | import kotlinddd.web.models.PayOrderRequest 11 | import org.axonframework.commandhandling.gateway.CommandGateway 12 | import org.springframework.http.HttpStatus 13 | import org.springframework.http.ResponseEntity 14 | import org.springframework.web.bind.annotation.* 15 | import java.util.* 16 | 17 | @RestController 18 | class OrderController(val commandGateway: CommandGateway) { 19 | // Commands 20 | @PostMapping("/orders") 21 | fun createOrder(@RequestBody request: CreateOrderRequest) : ResponseEntity { 22 | val command = CreateOrderCommand(UUID.fromString(request.customerId)) 23 | val orderId = commandGateway.sendAndWait(command) 24 | 25 | return ResponseEntity(orderId, HttpStatus.CREATED) 26 | } 27 | 28 | @PatchMapping("/orders/{orderId}/products") 29 | @ResponseStatus(HttpStatus.OK) 30 | fun addProduct(@PathVariable("orderId") orderId: String, @RequestBody request: AddProductRequest) { 31 | val command = AddProductCommand(UUID.fromString(orderId), 32 | UUID.fromString(request.productId), 33 | request.quantity) 34 | 35 | commandGateway.sendAndWait(command) 36 | } 37 | 38 | @PatchMapping("/orders/{orderId}/products/{productId}") 39 | @ResponseStatus(HttpStatus.OK) 40 | fun changeProductQuantity(@PathVariable("orderId") orderId: String, 41 | @PathVariable("productId") productId: String, 42 | @RequestBody request: ChangeProductQuantityRequest) { 43 | 44 | val command = ChangeProductQuantityCommand(UUID.fromString(orderId), 45 | UUID.fromString(productId), 46 | request.quantity) 47 | 48 | commandGateway.sendAndWait(command) 49 | } 50 | 51 | @DeleteMapping("/orders/{orderId}/products/{productId}") 52 | @ResponseStatus(HttpStatus.OK) 53 | fun removeProduct(@PathVariable("orderId") orderId: String, 54 | @PathVariable("productId") productId: String) { 55 | 56 | val command = RemoveProductCommand(UUID.fromString(orderId), UUID.fromString(productId)) 57 | 58 | commandGateway.sendAndWait(command) 59 | } 60 | 61 | @PatchMapping("/orders/{orderId}") 62 | @ResponseStatus(HttpStatus.OK) 63 | fun payOrder(@PathVariable("orderId") orderId: String, 64 | @RequestBody request: PayOrderRequest) { 65 | 66 | val card = CreditCard(request.cardName, request.cardNumber, request.expirationDate ?: Date(), request.verificationCode) 67 | val command = PayOrderCommand(UUID.fromString(orderId), card) 68 | 69 | commandGateway.sendAndWait(command) 70 | } 71 | 72 | // Queries 73 | @GetMapping("/orders/{orderId}") 74 | fun findOrderById(@PathVariable("orderId") orderId: String): OrderDTO { 75 | return OrdersQuery.findOrderById(UUID.fromString(orderId)) 76 | } 77 | 78 | @GetMapping("/orders", params = ["orders_per_users"]) 79 | fun findOrderPerUsers() : List{ 80 | return OrdersQuery.findOrderPerUsers() 81 | } 82 | 83 | @GetMapping("/orders", params = ["last_orders"]) 84 | fun findLastOrders() : List { 85 | return OrdersQuery.findLastOrders() 86 | } 87 | 88 | // @ResponseStatus(HttpStatus.BAD_REQUEST) 89 | // @ExceptionHandler(BusinessException::class) 90 | // fun handleException() { 91 | // 92 | // } 93 | // 94 | // @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 95 | // @ExceptionHandler(Exception::class) 96 | // fun handleException() { 97 | // 98 | // } 99 | } -------------------------------------------------------------------------------- /web/src/main/models/AddProductRequest.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.models 2 | 3 | data class AddProductRequest(val productId: String = "", val quantity: Int = 0) -------------------------------------------------------------------------------- /web/src/main/models/ChangeProductQuantityRequest.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.models 2 | 3 | data class ChangeProductQuantityRequest(val quantity: Int = 0) -------------------------------------------------------------------------------- /web/src/main/models/CreateOrderRequest.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.models 2 | 3 | data class CreateOrderRequest(val customerId: String = "") -------------------------------------------------------------------------------- /web/src/main/models/PayOrderRequest.kt: -------------------------------------------------------------------------------- 1 | package kotlinddd.web.models 2 | 3 | import java.util.* 4 | 5 | class PayOrderRequest( 6 | val cardName :String = "", 7 | val cardNumber :String = "", 8 | val expirationDate : Date? = null, 9 | val verificationCode :String = "" 10 | ) -------------------------------------------------------------------------------- /web/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | axon.amqp.exchange=Axon.EventBus 2 | axon.amqp.transaction-mode=publisher-ack 3 | axon.eventhandling.processors["kotlinddd.application.shipping.eventhandlers"].source=myRabbitMessageSource 4 | axon.eventhandling.processors["kotlinddd.application.shipping.eventhandlers"].mode=SUBSCRIBING 5 | spring.rabbitmq.host = 172.17.0.2 6 | spring.rabbitmq.port = 5672 7 | spring.rabbitmq.username = guest 8 | spring.rabbitmq.password = guest --------------------------------------------------------------------------------