├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── BPM-commanding-your-domain.png ├── fdd-design.png ├── fdd-git.png └── flow-as-domain-service.png ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lombok.config ├── order-preparation-poc-reactive ├── .gitignore ├── README.md ├── build.gradle └── src │ └── main │ ├── java │ └── com │ │ └── progmod │ │ └── poc │ │ ├── PocApplication.java │ │ ├── PocConfig.java │ │ ├── domain │ │ ├── Item.java │ │ ├── OrderPreparation.java │ │ ├── delegate │ │ │ ├── PickItemsDelegate.java │ │ │ ├── PickupDelegate.java │ │ │ ├── notifyDelegate.java │ │ │ └── startPreparationDelegate.java │ │ └── flow │ │ │ ├── OrderPreparationAction.java │ │ │ ├── OrderPreparationFlowType.java │ │ │ └── OrderPreparationState.java │ │ ├── dto │ │ ├── CreateOrderPreparationRequest.java │ │ ├── ItemRequest.java │ │ ├── PickItemRequest.java │ │ └── PickItemsRequest.java │ │ └── infra │ │ ├── primary │ │ ├── ErrorHandler.java │ │ └── PocController.java │ │ └── secondary │ │ └── KafkaEventPublisher.java │ └── resources │ ├── application.yaml │ ├── db │ └── migration │ │ └── V1__Initial_version.sql │ └── flow │ └── in-store-workflow.json ├── order-preparation-poc ├── .gitignore ├── README.md ├── build.gradle └── src │ └── main │ ├── java │ └── com │ │ └── progmod │ │ └── poc │ │ ├── PocApplication.java │ │ ├── PocConfig.java │ │ ├── domain │ │ ├── Item.java │ │ ├── OrderPreparation.java │ │ ├── delegate │ │ │ ├── PickItemsDelegate.java │ │ │ ├── PickupDelegate.java │ │ │ ├── notifyDelegate.java │ │ │ └── startPreparationDelegate.java │ │ └── flow │ │ │ ├── OrderPreparationAction.java │ │ │ ├── OrderPreparationFlowType.java │ │ │ └── OrderPreparationState.java │ │ ├── dto │ │ ├── CreateOrderPreparationRequest.java │ │ ├── ItemRequest.java │ │ ├── PickItemRequest.java │ │ └── PickItemsRequest.java │ │ └── infra │ │ ├── primary │ │ ├── ErrorHandler.java │ │ └── PocController.java │ │ └── secondary │ │ └── KafkaEventPublisher.java │ └── resources │ ├── application.yaml │ ├── db │ └── migration │ │ └── V1__Initial_version.sql │ └── flow │ └── in-store-workflow.json ├── settings.gradle └── tools ├── POC.postman_collection.json └── checkStyle └── sun_checks.xml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | bin/ 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | out/ 24 | !**/src/main/**/out/ 25 | !**/src/test/**/out/ 26 | 27 | ### NetBeans ### 28 | /nbproject/private/ 29 | /nbbuild/ 30 | /dist/ 31 | /nbdist/ 32 | /.nb-gradle/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | /ui/node_modules/ 37 | /ui/dist/ 38 | 39 | ### DB data ### 40 | tools/data 41 | tools/.docker/ 42 | 43 | ## Load test data ### 44 | tools/load-test/taurus/data 45 | 46 | .DS_Store 47 | **/.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Flow Driven Domain Software License 2 | 3 | IMPORTANT - READ CAREFULLY: 4 | This is a legal agreement between you and PROGMOD for "Flow Driven Domain," which includes software, media, and documentation. 5 | By using Flow Driven Domain, you agree to these terms. Do not use the software if you disagree with these terms. 6 | 7 | GRANT OF LICENSE: 8 | PROGMOD grants you a license to use Flow Driven Domain for internal business purposes only. You are not permitted to modify, distribute, sell, lease, rent, or sublicense the software. 9 | 10 | COPYRIGHT: 11 | Flow Driven Domain is protected by copyright laws and international copyright treaties, as well as other intellectual property laws and treaties. Flow Driven Domain is licensed, not sold. 12 | 13 | RESTRICTIONS: 14 | a. You may not reverse engineer, decompile, or disassemble Flow Driven Domain, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation. 15 | b. You may not distribute copies of Flow Driven Domain to third parties. 16 | c. You may not rent, lease, or lend Flow Driven Domain. 17 | 18 | TERMINATION: 19 | This license terminates if you violate its terms. Upon termination, destroy all copies of the software. 20 | 21 | NO WARRANTIES: 22 | Flow Driven Domain is provided "As Is" without any warranties. 23 | 24 | LIMITATION OF LIABILITY: 25 | PROGMOD is not liable for any damages arising from the use or inability to use the software. 26 | 27 | COPYRIGHT NOTICE: 28 | © 2024 Copyright PROGMOD 29 | All Rights Reserved 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Local Image](assets/fdd-git.png) 2 | Transform your Domain into a **process-centric domain**
3 | It enables you to send **actions** to your domain, put **rules** over these action's **transitions**, 4 | and separate process rules classes from your aggregate class and invariants.
5 | 6 | 🎯Two main purposes (2 points of vue): 7 | - **make your domain flowable** 8 | - **develop your domain as a process** 9 | 10 | > 💻Available in 11 | > - **reactive** functional programming (spring reactor) mode 12 | > - **normal** mode (imperative programming) 13 | > 14 | > Prerequisite: **JAVA 17** or higher 15 | 16 |
17 | 18 | 19 | 20 | 📚 **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 21 | 22 | - [Introduction](#-introduction) 23 | - [Hello World ex as tutorial](#-hello-world-ex-as-tutorial) 24 | - [Step1 - library dependency](#-step1---library-dependency) 25 | - [Step2 - Action, States and flow.json](#-step2---action-states-and-flowjson) 26 | - [Step3 - Implement Flowable interface](#-step3---implement-flowable-interface) 27 | - [Step4 - Implement delegates defined in flow.json](#-step4---implement-delegates-defined-in-flowjson) 28 | - [Step5 - Your domain Repository and your DataBase](#-step5---your-domain-repository-and-your-database) 29 | - [Implements the FlowRepository interface](#implements-the-flowrepository-interface) 30 | - [Create Your Domain DB table](#create-your-domain-db-table) 31 | - [Create the Tasks Table](#create-the-tasks-table) 32 | - [Step6 - Create the FlowEngine](#-step6---create-the-flowengine) 33 | - [Step7 - Make your domain flowable](#-step7---make-your-domain-flowable) 34 | - [Make your Domain Flowabe](#make-your-domain-flowabe) 35 | - [Invoke Actions](#invoke-actions) 36 | - [Step8 - Listen and publish flow events](#-step8---listen-and-publish-flow-events) 37 | - [Framework DDD Design](#-framework-ddd-design) 38 | - [Flow Functionalities](#-flow-functionalities) 39 | - [action](#action) 40 | - [state](#state) 41 | - [transition](#transition) 42 | - [Advantages](#-advantages) 43 | - [Use Cases](#-use-cases) 44 | - [Order Preparation POC](#-order-preparation-poc) 45 | - [Flow configuration](#-flow-configuration) 46 | - [Genesis and Achievement: Decathlon success story](#-genesis-and-achievement-decathlon-success-story) 47 | - [Get in Touch for Support and Collaboration](#-get-in-touch-for-support-and-collaboration) 48 | 49 | 50 | 51 | ## 🔍 Introduction 52 | Beyond basic business logic, domains often incorporate **state-based rules**.
53 | When these state-dependent rules come into the picture, the domain's overall structure 54 | becomes **more complex over time**, making both evolutionary changes and maintenance more difficult 55 | 56 | Managing the state complexity often leads us to use **state machines** or **business process management (BPM)** tools.
57 | While valuable, these tools operates as distinct units from the domain, leading to 58 | - potential inconsistencies 59 | - integration challenges 60 | - increased overhead 61 | - scattered business logic 62 | 63 | When using flow-driven-domain, your domain becomes more process-centric: 64 | - **Flow rules becomes first-call citizen in your your domain** 65 | - **Flow history is kept inside your domain model** 66 | 67 | Please read the [hello world](#hello-world-ex-as-tutorial) as tutorial. 68 | Then try a complete POC application [here](order-preparation-poc/README.md) 69 | 70 | ## 👋🌍 Hello World ex as tutorial 71 | 72 | Let's consider a HELLO WORLD example, where your domain is Greeting Aggregate with a simple Value Object message as String 73 | ```java 74 | public class Greeting { 75 | private UUIID id; 76 | private String message; 77 | 78 | public void updateMessage(String message) { 79 | this.message = message; 80 | } 81 | 82 | public void reset() { 83 | this.message = ""; 84 | } 85 | 86 | public String getMessage() { 87 | return message; 88 | } 89 | } 90 | ``` 91 | 92 | Now lets imagine we want to add some process/flow over our Greeting domain to be able to produce "Hello World" greeting message using 2 steps/actions 93 | - **HELLO**: an Action that set the message to "Hello" 94 | - **WORLD**: an Action that set the message to "Hello World" 95 | 96 | And imagine also we ave these rules:
97 | 1- WORLD action cant be executed without a previous HELLO action
98 | 2- after HELLO action, if WORLD action is not executed during 30 seconds, the Greeting Domain will reset
99 | 3- when WORLD is executed, the Greeting cant be changed anymore (reach a final State) 100 | 101 | **Lets explore how we can use the framework to reach our goal.** 102 | 103 | ### 📦 Step1 - library dependency 104 | Add the library dependency, here is a gradle example: 105 | 106 | ```gradle 107 | repositories { 108 | mavenCentral() 109 | } 110 | 111 | dependencies { 112 | implementation "io.github.progmodek:flow-reactive:1.0.1" // "io.github.progmodek:flow:1.0.1" for non-reactive 113 | } 114 | ``` 115 | 116 | ### 🎛️ Step2 - Action, States and flow.json 117 | 118 | Define ACTIONS, STATES and a process/flow JSON file based on these ENUMs. 119 | 120 | - **ACTION ENUM** 121 | 122 | Define a java enum containing the actions your process supports 123 | ```java 124 | public enum GreetingAction implements FlowAction { 125 | HELLO(USER), 126 | WORLD(USER), 127 | TIMEOUT(SYSTEM); 128 | private final ActionType type; 129 | } 130 | ``` 131 | 132 | > USER vs SYSTEM indicates if its a user action or system action (TIMEOUT is a system action invoked automatically by the system). 133 | 134 | - **STATE ENUM** 135 | 136 | Define a java enum containing all the states of your process 137 | ```java 138 | public enum GreetingState implements FlowState { 139 | INITIAL, 140 | PENDING_COMPLETION, 141 | EXPIRED, 142 | COMPLETED 143 | } 144 | ``` 145 | > INITIAL : initial state when we create our Greeting domain
146 | PENDING_COMPLETION : intermediate state(after a HELLO action)
147 | EXPIRED, COMPLETED: final states where no more actions are acceptable 148 | 149 | - **FlowType ENUM** 150 | 151 | This enum lets you define different flow type (ex DEFAULT), each flow type based on the action and state enum.
152 | 153 | ```java 154 | public enum GreetinglowType implements FlowType { 155 | DEFAULT("default-flow.json", GreetingAction.class, GreetingState.class); 156 | 157 | @Getter 158 | private final String template; 159 | @Getter 160 | private final Class flowActionType; 161 | @Getter 162 | private final Class flowStateType; 163 | 164 | } 165 | ``` 166 | > This means that DEFAULT flowType is based on GreetingAction and GreetingState enums, and presented by default-flow.json file.
167 | > you can have for ex another type EXPRESS with another Json file defining another flow. 168 | 169 | - flow **Json file** (default-flow.json in our case) 170 | ```json 171 | { 172 | "actions": [ 173 | { 174 | "name": "HELLO", 175 | "delegate": "helloDelegate" 176 | }, 177 | { 178 | "name": "WORLD", 179 | "delegate": "worldDelegate" 180 | }, 181 | { 182 | "name": "TIMEOUT", 183 | "delegate": "timeoutDelegate", 184 | "expiration": true 185 | } 186 | ], 187 | "states": [ 188 | { 189 | "name": "INITIAL", 190 | "initial": true, 191 | "transitions": [ 192 | { 193 | "action": "HELLO", 194 | "internal": "PENDING_COMPLETION" 195 | } 196 | ] 197 | }, 198 | { 199 | "name": "PENDING_COMPLETION", 200 | "transitions": [ 201 | { 202 | "action": "WORLD", 203 | "result": { 204 | "success": "COMPLETED" 205 | } 206 | }, 207 | { 208 | "action": "TIMEOUT", 209 | "result": { 210 | "success": "EXPIRED" 211 | }, 212 | "timer": { 213 | "sec": 30 214 | } 215 | } 216 | ] 217 | }, 218 | { 219 | "name": "EXPIRED", 220 | "transitions": [] 221 | }, 222 | { 223 | "name": "COMPLETED", 224 | "transitions": [] 225 | } 226 | ] 227 | } 228 | ``` 229 | > Explanations:
230 | --We list the ACTIONS with their associated delegates (these delegates will be invoked when the action is invoked)
231 | --**TIMEOUT** action have also "expiration=true", this tells that this action is used for expiration and the system will be able to compute "expiresAt" automatically based on the state you currently are.
232 | --**INITIAL** state has "initial=true", means when we create our Greeting Aggregate it will be in an **INITIAL** state.
233 | --**INITIAL** state have only 1 transition with the action **HELL**O and transit to **PENDING_COMPLETION** state.
234 | --**PENDING_COMPLETION** has 2 transitions, one with action **WORLD** that transit to **COMPLETED** state, and one with **TIMEOUT** (this one is automatic with a **timer** of 30sec), 235 | means that while in **PENDING_COMPLETION** state, if no **WORLD** action is called, it will automatically transit to **EXPIRED** after 30 sec 236 | 237 | ### ⚙️ Step3 - Implement Flowable interface 238 | 239 | Let your Domain Aggregate Implement the Flowable Interface 240 | ```java 241 | public interface Flowable { 242 | ID getId(); 243 | String getState(); 244 | void setState(String state); 245 | Flow getFlow(); 246 | void setFlow(Flow flow); 247 | } 248 | ``` 249 | > The Flowable interface requires the implementation of getters and setters for ID, State, and Flow properties, which are essential for a process-centric model. 250 | 251 | So the greeting becomes: 252 | ```java 253 | @Data 254 | public class Greeting implements Flowable { 255 | // your aggregate ID 256 | private UUID id; 257 | private String message; 258 | 259 | // ----------------------------------------------------- 260 | // flow elements to add to your domain 261 | // remember that your domain becomes process-centric, 262 | // means it holds the flow history 263 | private String state; 264 | private Flow flow; 265 | // ----------------------------------------------------- 266 | 267 | public void updateMessage(String message) { 268 | this.message = message; 269 | } 270 | 271 | public String getMessage() { 272 | return message; 273 | } 274 | } 275 | ``` 276 | > **NB**: you have to persist these property in your repository
277 | if you are using NOSQL it will be straight forward,
278 | if not you must persist Flow property as json or jsonb 279 | 280 | ### ✍️ Step4 - Implement delegates defined in flow.json 281 | 282 | Define an ActionDelegate for each **Action**.
283 | these delegate encapsulate the necessary business logic for the corresponding action. 284 | ```java 285 | @Component 286 | public class HelloDelegate implements ActionDelegate { 287 | 288 | @Override 289 | public Mono execute(final Greeting greeting, final Map variables, final DelegateParams delegateParams) { 290 | // implement process related logic, ex call a API, etc.. 291 | // and invoke(or not) a behavior on our aggregate 292 | greeting.updateMessage("Hello"); 293 | return Mono.just(greeting); 294 | } 295 | 296 | } 297 | 298 | @Component 299 | public class WorldDelegate implements ActionDelegate { 300 | 301 | @Override 302 | public Greeting execute(final Greeting greeting, final Map variables, final DelegateParams delegateParams) { 303 | // implement process related logic, ex call a API, etc.. (in our case nothing special) 304 | // and invoke(or not) a behavior on our aggregate (in our case we update the message) 305 | greeting.updateMessage("Hello World"); 306 | return greeting; 307 | } 308 | 309 | } 310 | 311 | @Component 312 | public class TimeoutDelegate extends SystemActionDelegate { 313 | 314 | @Override 315 | public Greeting execute(final Greeting greeting, final Map variables) { 316 | // implement process related logic, ex call a API, etc.. (in our case nothing special) 317 | // and invoke(or not) a behavior on our aggregate (in our case we reset the aggregate after a timeout) 318 | greeting.reset(); 319 | return greeting; 320 | } 321 | 322 | } 323 | ``` 324 | > Each delegate must implement the generic **ActionDelegate**
325 | -**T** type is your aggregate
326 | -**I** type is the input that you can pass to the delegate (a request object containing params)
327 | -**R** type is the return type (in our ex its the aggregate itself, but you can return a response from calling an external api for ex)
328 | 329 | > **Note**: for the system action **TIMEOUT" we extends **SystemActionDelegate** which is a special implementation of ActionDelegate without input params and the aggregate as return value 330 | 331 | 332 | ### 🗄️ Step5 - Your domain Repository and your DataBase 333 | 334 | #### Implements the FlowRepository interface 335 | Let your **DomainRepository** extends this **FlowRepository** interface
336 | 337 | ```java 338 | public interface FlowRepository { 339 | Optional findById(ID flowId); 340 | T save(T flowable); 341 | } 342 | ``` 343 | > This interface contains only the already used method in Spring Repositories so it should be straight froward.
344 | Means if you are using Spring Data or Spring JPA these methods are already handled 345 | 346 | ex for reactive mode : 347 | ```java 348 | public interface DomainRepository extends ReactiveCrudRepository, FlowRepository { 349 | } 350 | ``` 351 | or JPA mode 352 | ```java 353 | public interface DomainRepository extends JpaRepository, FlowRepository { 354 | } 355 | ``` 356 | 357 | > the framework includes a Base FlowRepository implementation in case you are using postgres jsonb 358 | > so you can use it to define your domain repository as follow: 359 | ```java 360 | @Bean 361 | public FlowRepository greetingPostGresRepository() { 362 | return new BaseR2dbcPostgresJsonRepository<>(Greeting.class) 363 | .setTableInfo(table_name, jsonb_column); 364 | } 365 | ``` 366 | 367 | #### Create Your Domain DB table 368 | 369 | > the **Flow** property should always be in JSON format 370 | > Best Practice: use JSONB if you are using postgres 371 | 372 | Following are some example using postgres JSONB
373 | If you are using JSONB for all your aggregate, your table would be 374 | ```roomsql 375 | CREATE TABLE IF NOT EXISTS greeting ( 376 | id uuid NOT NULL, 377 | greeting_data jsonb NOT NULL, 378 | CONSTRAINT flow_pk PRIMARY KEY (id) 379 | ); 380 | ``` 381 | 382 | If you are using JSONB only for the **Flow** property of your aggregate, your table would be 383 | ```roomsql 384 | CREATE TABLE IF NOT EXISTS greeting ( 385 | id uuid NOT NULL, 386 | message varchar NOT NULL, 387 | flow_data jsonb NOT NULL, 388 | CONSTRAINT flow_pk PRIMARY KEY (id) 389 | ); 390 | ``` 391 | 392 | > you can off course use a DB other than postgresql 393 | 394 | #### Create the Tasks Table 395 | create a table where scheduled tasks will be saved (flow capabilities like timeouts or other scheduled automatic tasks) 396 | ex for postgres 397 | ```roomsql 398 | CREATE TABLE IF NOT EXISTS flow_task ( 399 | id varchar NOT NULL, 400 | score int8 NOT NULL, 401 | status VARCHAR(255) NOT NULL, 402 | ver INTEGER NOT NULL, 403 | PRIMARY KEY (id) 404 | ); 405 | ``` 406 | > Replace the types by your specific database types (ex VARCHAR(255), BIGINT, INTEGER) 407 | 408 | ### 🚀 Step6 - Create the FlowEngine 409 | 410 | The FlowEngine is pivotal in managing the lifecycle of flowable domain objects, interfacing with the repository to maintain state consistency and handle transitions.
411 | In a DDD point of vue it is the application layer (check [framework design](#framework-ddd-design)). 412 | 413 | Instantiate a FlowEngine with the:
414 | -**ID class type** of your Aggregate
415 | -**Aggragte class type**
416 | -**Repository** of your domain 417 | 418 | ```java 419 | @Bean 420 | FlowEngine flowEngine(DomainRepository repo) { 421 | return new FlowEngine<>(UUID.class, Greeting.class, repo); 422 | } 423 | ``` 424 | 425 | ### 🔄 Step7 - Make your domain flowable 426 | Make your domain flowable and execute actions on your domain 427 | 428 | #### Make your Domain Flowabe 429 | ```java 430 | Greeting greeting = 'create your Aggragte' 431 | flowEngine.makeFlowable(greeting, ProductflowType.DEFAULT, Map.of()); 432 | ``` 433 | > we can pass a MAP that will be saved to the variables property of the Flow if we want for later use 434 | #### Invoke Actions 435 | ```java 436 | flowEngine.applyAction(id, HELLO, inputParams) 437 | ``` 438 | and here you go, your domain will be process-centric (check the state and the flow properties) 439 | ```json 440 | { 441 | "id": "e30d2da8-f18e-4c43-bd84-bcffb726cb37", 442 | "message" : "Hello", 443 | "flow": { 444 | "flowType": "DEFAULT", 445 | "eligibleActions": ["WORLD"], 446 | "actions": [ 447 | { 448 | "name": "HELLO", 449 | "type": "USER", 450 | "count": 1, 451 | "executions": [ 452 | { 453 | "executedAt": "2023-08-23T15:48:11.091511Z", 454 | "result": "success", 455 | "previousState": "INITIAL", 456 | "nextState": "PENDING_COMPLETE" 457 | } 458 | ] 459 | } 460 | ] 461 | }, 462 | "state": "PENDING_COMPLETE" 463 | } 464 | ``` 465 | > we can see when the **HELLO** action was executed and how the state arrived to **PENDING_COMPLETE** and we see too that the Greeting will expire in 30sec with the **expiresAt** property 466 | 467 | ### 📣 Step8 - Listen and publish flow events 468 | 469 | Each executed **Action** is considered as a **flow Event**.
470 | All flow events are pushed to **EventsPublishers**, implementing custom event publishers allows you to have an event-driven architecture.
471 | You can define your own Event Publisher:
472 | 473 | ```java 474 | @Component 475 | @Slf4j 476 | public class KafkaEventPublisher implements EventsPublisher { 477 | @Override 478 | public void publishEvents(final Flowable aggregate) { 479 | log.info("PUBLISH EVENTS TO KAFKA IF YOU WANT"); 480 | } 481 | } 482 | ``` 483 | 484 | > The framework includes an ActionLogger as an EventsPublisher which logs all the executedAction.
485 | > You can define your own Logger as EventsPublisher and disable the framework one 486 | 487 | 488 | ## 🏗️ Framework DDD Design 489 | 490 | The framework design follows DDD principles.
491 | - Your Aggregate (your principal entity), is at the heart of the domain 492 | - The Framework flowEngine acts as the Application layer of your domain 493 | - The flow related mechanism (actions eligibility, transitions, etc..) is coded as a Domain Service
494 | 495 | > BLUE : Framework Components
496 | > GREEN : Your components 497 | 498 | 499 | ![Local Image](assets/fdd-design.png) 500 | 501 | > the **Port/Interface** part that links the domain to the infra layer is not shown to simplify the diagram
502 | 503 | ## 🛠️ Flow Functionalities 504 | 505 | We saw in HelloWorld example some features of the **flow.json** when defining actions, states and transitions.
506 | Here is a List of most of the features and how to use them 507 | 508 | ### action 509 | 510 | we can use these properties on each **action** defined in the **JSON** file 511 | - **delegate**: the spring bean (implementation of an ActionDelegate) that will be invoked 512 | - **expiration**: ture/false(default) used to compute an **expiredAt** functionality (inside the **flow** property of the aggregate) 513 | 514 | ```json 515 | { 516 | "name": "TIMEOUT", 517 | "delegate": "timeoutDelegate", 518 | "expiration": true 519 | } 520 | ``` 521 | 522 | > When defined as "expiration" action, whenever we are in a state including a transition with a TIMEOUT action, an "expiresAt" property will be computed, 523 | > and users knows explicitly when the process will expire. 524 | 525 | ### state 526 | 527 | we can use these properties on each **state** defined in the **JSON** file 528 | - **initial**: ture/false(default) to indicate that it will be the INITIAL state of the aggregate
529 | Remember how we make the aggregate flowable 530 | ```java 531 | Greeting greeting = 'create your Aggragte' 532 | flowEngine.makeFlowable(greeting, ProductflowType.DEFAULT, Map.of()); 533 | ``` 534 | 535 | ```json 536 | { 537 | "name": "INITIAL", 538 | "initial": true, 539 | } 540 | ``` 541 | 542 | ### transition 543 | 544 | we can use these properties on each **transition** defined in the **JSON** file 545 | - **internal**: indicates that the transition is internal, means the state will remains the same 546 | - **dependsOn**: indicates that the transition depends on a preceding action. normally used with **internal** transitions 547 | ex: if we have 3 internal transition with A1,A2 and A3, we can say that the 2nd transition using A2 dependsOn A1 548 | - **result**: this String based map indicates where to transit based on functional results (success, waiting, error, etc..) 549 | - **timer**: indicates that the action will be automatically executed after x sec 550 | - **exceptions**: indicates where to transit based on exceptions (exceptions can be launched from action delegates using the **DelegateException**) 551 | - **retry**: number of retries (in case of exceptions) before transiting to another state 552 | 553 | ```json 554 | "transitions": [ 555 | { 556 | "action": "APPROVE", 557 | "result": { 558 | "success": "APPROVED", 559 | "error": "NOT_APPROVED" 560 | }, 561 | "exceptions": { 562 | "001": "PENDING_APPROVAL" 563 | }, 564 | "retry": { 565 | "number": 3, 566 | "exceeded": "NOT_APPROVED" 567 | }, 568 | "timer": { 569 | "sec": 30 570 | } 571 | } 572 | ] 573 | ``` 574 | 575 | > **APPROVE** action will be launched in **30sec** (**timer**)
576 | > if the **functional** result (fom the actionDelegate) is **success** then transit to **APPROVED**
577 | > If the **functional** result (fom the actionDelegate) is **error** then transit to **NOT_APPROVED**
578 | > if an **exception** is thrown (**technical** exception from the delegate) then transit to **PENDING_APPROVAL**
579 | > in case of other *$exceptions** (not handled by the exceptions part) occurred then stay in same state, but after **3 retries** transit to **NOT_APPROVED**
580 | 581 | 582 | ## ✅ Advantages 583 | 584 | - **Consistency and audability**: 585 | - Enables easy tracking and auditing of all state changes 586 | - With state and transition history within the domain, there’s no loss of data or inconsistencies 587 | - **Unit testing**: 588 | - Test youre aggregate behavior independently of process behaviour 589 | - Test your process rules by testing each sepearate delegate 590 | - **Synchronous user request-response capabilities**: 591 | - FlowEngine can apply an action and its delegate and the delegate can provide an object response to the client 592 | - Improve system-user interactions and provides UX flexibility 593 | - **Asynchronous system action capabilities**: 594 | - Define system-triggered Actions, like timeouts, to automate specific flow transitions 595 | - Non blocking operations: by handling actions asynchronously, the system can continue performing other actions, improving overall efficiency 596 | - **Usefull process-centric properties**: 597 | - **expiresAT**: let users know when and if the process will expire 598 | - **eligibleActions**: gives the user to know wihc possible actions are allowed (depending on the state) 599 | - **Easy to use and fun to integrate**: 600 | - Intuitive design base on Enums and JSON workflow descriptors 601 | - Seamless integration with only some few components to configure 602 | - **Cost & time effective**: 603 | - Reduces the overhead of managing separate BPM tools 604 | 605 | 606 | ## 💼 Use Cases 607 | 608 | Here is a list of some use cases that can be thought as a flow driven domain: 609 | 610 | - **Checkout Process** (process-centric model of an order being placed) 611 | - Domain Aggregate: Checkout implements Flowable {} 612 | - Domain Entities: Items, shippings ,payments 613 | - Flow states: PENDING_SHIPPING, PENDING_PAYMENT, COMPLETED, EXPIRED, etc.. 614 | 615 | 616 | - **Order Preparation** (check the full application [here](#poc-order-preparation)) 617 | - Domain Aggregate: OrderPreparation implements Flowable {} 618 | - Domain Entities: Items 619 | - Flow states: TO_PREPARE, IN_PREPARATION, PENDING_PICKUP, DELIVERED, COMPLETED, etc. 620 | 621 | 622 | - **Product Lifecycle** (representing the process/lifecycle of a product design and development) 623 | - Domain Aggregate: ProductDesign implements Flowable {} 624 | - Domain Entities: InitialDesign, Prototype, FeedBack, etc.. 625 | - Flow states: PENDING_PROTOTYPING, PENDING_TEST, PRODUCED, STOPPED, etc. 626 | 627 | 628 | - **Job Application Process** 629 | - Domain Aggregate: JobApplication implements Flowable {} (representing the entire lifecycle of the job application) 630 | - Domain Entities: ApplicantProfile, JobPosition, Evaluation, etc.. 631 | - Flow states: PENDING_REVIEW, ACCEPTED, REJECTED, etc. 632 | 633 | 634 | - **And much more…** (every domain can have its own process-centric view) 635 | 636 | ## 📝 Order Preparation POC 637 | 638 | A complete POC application for Preparation of Orders in a click-and-collect retail environment can be found [here](order-preparation-poc/README.md) 639 | > Prerequisite: **JAVA 17** or higher 640 | 641 | ## 🔧 Flow configuration 642 | 643 | here are some properties to configure in **application.yaml** 644 | > Default values are already configured, use only if you want to override 645 | 646 | ```yaml 647 | flow: 648 | actionLogger: 649 | enabled: true // Default Logger component enabled by default 650 | taskConsumer: 651 | scheduleMilli: 1000 // schedule for pooling for async tasks (the system tasks that should be executed automatically) 652 | ``` 653 | 654 | ## 🌟 Genesis and Achievement: Decathlon success story 655 | 656 | The foundational **concept of the Framework** was originally crafted for **Decathlon**.
657 | It was specifically tailored to address their **checkout process** needs and challenges.
658 | 659 | This concept, a testament to our innovative approach, has been successfully implemented within Decathlon, demonstrating its effectiveness and reliability in a real-world, enterprise environment.
660 | 661 | Building on this initial success, the concept was further evolved into a full-fledged framework, 662 | guided by principles of Domain-Driven Design (DDD), ensuring a robust and scalable architecture. 663 | 664 | ## 🤝 Get in Touch for Support and Collaboration 665 | 666 | If you have any questions, need assistance with the framework, or want to discuss potential projects, please don't hesitate to reach out.
667 | We value your feedback and are eager to support your projects using this framework.
668 | Let's innovate and create together! 669 | - mail : elie.khoury.progmod@gmail.com -------------------------------------------------------------------------------- /assets/BPM-commanding-your-domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progmodEK/flow-driven-domain/9eb6046afb2402d345805b7ef2e7a5572d6e1a3b/assets/BPM-commanding-your-domain.png -------------------------------------------------------------------------------- /assets/fdd-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progmodEK/flow-driven-domain/9eb6046afb2402d345805b7ef2e7a5572d6e1a3b/assets/fdd-design.png -------------------------------------------------------------------------------- /assets/fdd-git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progmodEK/flow-driven-domain/9eb6046afb2402d345805b7ef2e7a5572d6e1a3b/assets/fdd-git.png -------------------------------------------------------------------------------- /assets/flow-as-domain-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progmodEK/flow-driven-domain/9eb6046afb2402d345805b7ef2e7a5572d6e1a3b/assets/flow-as-domain-service.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres 6 | environment: 7 | POSTGRES_USER: ${POSTGRES_USER:-admin} 8 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin} 9 | PGDATA: ./tools/data/postgres 10 | volumes: 11 | - type: bind 12 | source: ./tools/data/postgres-data 13 | target: /var/lib/postgresql/data 14 | ports: 15 | - "5432:5432" 16 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progmodEK/flow-driven-domain/9eb6046afb2402d345805b7ef2e7a5572d6e1a3b/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-8.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /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 Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | # tells Lombok that this is the root directory and that it shouldn’t search parent directories for more configuration files 2 | config.stopBubbling = true 3 | # tells Lombok to add @lombok.Generated annotation to all generated methods 4 | # --> This is used to tell sonar to ignore lombok annotations in code coverage. 5 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /order-preparation-poc-reactive/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | bin/ 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | out/ 24 | !**/src/main/**/out/ 25 | !**/src/test/**/out/ 26 | 27 | ### NetBeans ### 28 | /nbproject/private/ 29 | /nbbuild/ 30 | /dist/ 31 | /nbdist/ 32 | /.nb-gradle/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | /ui/node_modules/ 37 | /ui/dist/ 38 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Order Preparation POC - Reactive 3 | 4 | This is the reactive version of the same application [order preparation poc](../order-preparation-poc/README.md) -------------------------------------------------------------------------------- /order-preparation-poc-reactive/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "io.freefair.lombok" version "8.4" 4 | id "org.springframework.boot" version "3.1.2" 5 | id "io.spring.dependency-management" version "1.1.4" 6 | } 7 | 8 | group "com.progmod.poc" 9 | version "0.0.1-SNAPSHOT" 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | // Flow Driven Domain Library 17 | implementation "io.github.progmodek:flow-reactive:1.0.1" 18 | 19 | // ------------------------ 20 | // Application Dependencies 21 | // ------------------------ 22 | //Spring 23 | implementation "org.springframework.boot:spring-boot-starter-webflux" 24 | 25 | // Postgres 26 | implementation "org.postgresql:r2dbc-postgresql" 27 | runtimeOnly "org.postgresql:postgresql" 28 | 29 | //flyway migration 30 | runtimeOnly "org.flywaydb:flyway-core" 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/PocApplication.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class PocApplication { 10 | public static void main(final String[] args) { 11 | SpringApplication.run(PocApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/PocConfig.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc; 2 | 3 | import com.progmod.flow.domain.port.FlowRepository; 4 | import com.progmod.flow.domain.service.FlowEngine; 5 | import com.progmod.flow.infra.database.BaseR2dbcPostgresJsonRepository; 6 | import com.progmod.poc.domain.OrderPreparation; 7 | import com.progmod.poc.domain.flow.OrderPreparationFlowType; 8 | import java.util.UUID; 9 | import org.springframework.context.ApplicationContext; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | public class PocConfig { 15 | 16 | @Bean 17 | public FlowRepository orderPostgresRepository() { 18 | return new BaseR2dbcPostgresJsonRepository<>(OrderPreparation.class) 19 | .setTableInfo("order_preparation", "data"); 20 | } 21 | 22 | @Bean 23 | FlowEngine flowEngine(final ApplicationContext applicaioncontext) { 24 | return new FlowEngine<>(UUID.class, OrderPreparationFlowType.class, orderPostgresRepository()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/Item.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Builder 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class Item { 13 | String skuId; 14 | String name; 15 | int qty; 16 | int qtyPrepared; 17 | boolean pickedUp; 18 | 19 | 20 | /** 21 | * Update Prepared Qty and invariants. 22 | */ 23 | public Item updatePreparedQty(final int qtyPrepared) { 24 | if (qtyPrepared > this.qty) { 25 | throw new IllegalArgumentException("prepared qty is greater than order qty"); 26 | } 27 | this.qtyPrepared = qtyPrepared; 28 | if (qty == qtyPrepared) { 29 | pickedUp = true; 30 | } 31 | return this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/OrderPreparation.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 4 | import com.progmod.flow.domain.model.Flow; 5 | import com.progmod.flow.domain.model.Flowable; 6 | import com.progmod.poc.dto.PickItemRequest; 7 | import com.progmod.poc.dto.PickItemsRequest; 8 | import java.beans.Transient; 9 | import java.util.List; 10 | import java.util.UUID; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | @JsonPropertyOrder({"id", "orderRef", "items", "state", "flow"}) 21 | public class OrderPreparation implements Flowable { 22 | @Builder.Default 23 | private UUID id = UUID.randomUUID(); 24 | private String orderRef; 25 | private List items; 26 | 27 | private String state; 28 | private Flow flow; 29 | 30 | 31 | /** 32 | * Update Preparation and check Aggregate invariants. 33 | */ 34 | public void updatePreparation(final PickItemsRequest pickItemsRequest) { 35 | // check picked items exist 36 | final List pickedSkus = pickItemsRequest.pickItems().stream() 37 | .map(PickItemRequest::skuId) 38 | .toList(); 39 | final List existingSkus = items.stream() 40 | .map(Item::getSkuId) 41 | .toList(); 42 | final boolean allExists = pickedSkus.stream().allMatch(s -> existingSkus.contains(s)); 43 | if (!allExists) { 44 | throw new IllegalArgumentException("prepared items does not exists"); 45 | } 46 | 47 | // update Items preparation 48 | pickItemsRequest.pickItems().stream() 49 | .forEach( 50 | pickItemRequest -> items.stream().filter(item -> item.skuId.equals(pickItemRequest.skuId())).findFirst() 51 | .map(item -> item.updatePreparedQty(pickItemRequest.qty())) 52 | ); 53 | } 54 | 55 | @Transient 56 | public boolean isFullyPrepared() { 57 | return items.stream() 58 | .filter(item -> item.qty == item.qtyPrepared) 59 | .toList().size() == items.size(); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/delegate/PickItemsDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import static com.progmod.flow.utils.FlowUtils.ACTION_TRANSITION_VARIABLE; 4 | 5 | import com.progmod.flow.domain.service.delegate.ActionDelegate; 6 | import com.progmod.poc.domain.OrderPreparation; 7 | import com.progmod.poc.dto.PickItemsRequest; 8 | import java.util.Map; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.log4j.Log4j2; 11 | import org.springframework.stereotype.Component; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Component 15 | @RequiredArgsConstructor 16 | @Log4j2 17 | public class PickItemsDelegate implements ActionDelegate { 18 | 19 | @Override 20 | public Mono execute(final OrderPreparation orderPreparation, 21 | final Map actionContext, 22 | final PickItemsRequest pickItemsRequest) { 23 | orderPreparation.updatePreparation(pickItemsRequest); 24 | actionContext.put(ACTION_TRANSITION_VARIABLE, orderPreparation.isFullyPrepared() ? "full" : "partial"); 25 | return Mono.just(orderPreparation); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/delegate/PickupDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import com.progmod.flow.domain.service.delegate.ActionDelegate; 4 | import com.progmod.poc.domain.OrderPreparation; 5 | import java.util.Map; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.stereotype.Component; 9 | import reactor.core.publisher.Mono; 10 | 11 | @Component 12 | @RequiredArgsConstructor 13 | @Log4j2 14 | public class PickupDelegate implements ActionDelegate, OrderPreparation> { 15 | 16 | 17 | @Override 18 | public Mono execute(final OrderPreparation order, final Map variables, 19 | final Map inputParams) { 20 | log.info("pickup invoked"); 21 | return Mono.just(order); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/delegate/notifyDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import com.progmod.flow.domain.service.DelegateException; 4 | import com.progmod.flow.domain.service.delegate.SystemActionDelegate; 5 | import com.progmod.poc.domain.OrderPreparation; 6 | import java.util.Map; 7 | import java.util.Random; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.springframework.stereotype.Component; 11 | import reactor.core.publisher.Mono; 12 | 13 | @Component 14 | @RequiredArgsConstructor 15 | @Log4j2 16 | public class notifyDelegate extends SystemActionDelegate { 17 | 18 | 19 | @Override 20 | public Mono execute(final OrderPreparation orderPreparation, final Map variables) { 21 | log.info("notification invoked"); 22 | // simulate a 50% error to test the flow 23 | if ((new Random()).nextBoolean()) { 24 | return Mono.error(new DelegateException(1, "notification error")); 25 | } else { 26 | return Mono.just(orderPreparation); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/delegate/startPreparationDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import com.progmod.flow.domain.service.delegate.ActionDelegate; 4 | import com.progmod.poc.domain.OrderPreparation; 5 | import java.util.Map; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.stereotype.Component; 9 | import reactor.core.publisher.Mono; 10 | 11 | @Component 12 | @RequiredArgsConstructor 13 | @Log4j2 14 | public class startPreparationDelegate 15 | implements ActionDelegate, OrderPreparation> { 16 | 17 | @Override 18 | public Mono execute(final OrderPreparation orderPreparation, 19 | final Map actionContext, 20 | final Map inputParams) { 21 | log.info("startPreparation invoked"); 22 | return Mono.just(orderPreparation); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/flow/OrderPreparationAction.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.flow; 2 | 3 | import static com.progmod.flow.domain.model.ActionType.SYSTEM; 4 | import static com.progmod.flow.domain.model.ActionType.USER; 5 | 6 | import com.progmod.flow.domain.model.ActionType; 7 | import com.progmod.flow.domain.service.parser.definition.FlowAction; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | /** 12 | * Enumeration declaring all available actions name inside a flow. 13 | */ 14 | @Getter 15 | @RequiredArgsConstructor 16 | public enum OrderPreparationAction implements FlowAction { 17 | START_PREPARATION(USER), 18 | PICK_ITEMS(USER), 19 | PICKUP(USER), 20 | NOTIFY_OM(SYSTEM); 21 | 22 | private final ActionType type; 23 | } 24 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/flow/OrderPreparationFlowType.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.flow; 2 | 3 | import com.progmod.flow.domain.service.parser.definition.FlowAction; 4 | import com.progmod.flow.domain.service.parser.definition.FlowState; 5 | import com.progmod.flow.domain.service.parser.definition.FlowType; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | 9 | @AllArgsConstructor 10 | public enum OrderPreparationFlowType implements FlowType { 11 | DEFAULT("flow/in-store-workflow.json", OrderPreparationAction.class, OrderPreparationState.class); 12 | 13 | @Getter 14 | private final String template; 15 | @Getter 16 | private final Class flowActionType; 17 | @Getter 18 | private final Class flowStateType; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/domain/flow/OrderPreparationState.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.flow; 2 | 3 | import com.progmod.flow.domain.service.parser.definition.FlowState; 4 | import lombok.Getter; 5 | 6 | /** 7 | * Enumeration declaring all available states for the flow. 8 | */ 9 | @Getter 10 | public enum OrderPreparationState implements FlowState { 11 | TO_PREPARE, 12 | IN_PREPARATION, 13 | PENDING_PICKUP, 14 | DELIVERED, 15 | RETRY_NOTIFICATION, 16 | COMPLETED, 17 | ERROR 18 | } 19 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/dto/CreateOrderPreparationRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public record CreateOrderPreparationRequest(@JsonProperty("orderRef") String orderRef, 7 | @JsonProperty("items") List items) { 8 | } 9 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/dto/ItemRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record ItemRequest(@JsonProperty("skuId") String skuId, @JsonProperty("name") String name, 6 | @JsonProperty("qty") int qty) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/dto/PickItemRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record PickItemRequest(@JsonProperty("skuId") String skuId, @JsonProperty("qty") int qty) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/dto/PickItemsRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public record PickItemsRequest(@JsonProperty("pickItems") List pickItems) { 7 | } 8 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/infra/primary/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.infra.primary; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.springframework.http.HttpStatusCode; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * Class defined to manage all controller exceptions, error response and status 12 | */ 13 | @Log4j2 14 | @ControllerAdvice 15 | public class ErrorHandler { 16 | 17 | //////////////// 18 | // 4xx errors // 19 | //////////////// 20 | @ExceptionHandler 21 | Mono> handle(final RuntimeException exception) { 22 | return Mono.just(ResponseEntity.status(HttpStatusCode.valueOf(406)).body(exception.getMessage())); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/infra/primary/PocController.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.infra.primary; 2 | 3 | import com.progmod.flow.domain.port.FlowRepository; 4 | import com.progmod.flow.domain.service.FlowEngine; 5 | import com.progmod.poc.domain.Item; 6 | import com.progmod.poc.domain.OrderPreparation; 7 | import com.progmod.poc.domain.flow.OrderPreparationAction; 8 | import com.progmod.poc.domain.flow.OrderPreparationFlowType; 9 | import com.progmod.poc.dto.CreateOrderPreparationRequest; 10 | import com.progmod.poc.dto.PickItemsRequest; 11 | import java.util.Map; 12 | import java.util.UUID; 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.extern.log4j.Log4j2; 15 | import org.springframework.context.ApplicationContext; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.PostMapping; 20 | import org.springframework.web.bind.annotation.RequestBody; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.RestController; 23 | import reactor.core.publisher.Mono; 24 | 25 | @RestController 26 | @Log4j2 27 | @RequestMapping("/pocs") 28 | @RequiredArgsConstructor 29 | public class PocController { 30 | 31 | protected final FlowRepository orderPreparationRepository; 32 | protected final FlowEngine flowEngine; 33 | private ApplicationContext applicaioncontext; 34 | 35 | @PostMapping 36 | public Mono> createOrderPreparation( 37 | @RequestBody final CreateOrderPreparationRequest createProductProcessRequest) { 38 | 39 | final OrderPreparation orderPreparation = OrderPreparation.builder() 40 | .orderRef(createProductProcessRequest.orderRef()) 41 | .items(createProductProcessRequest.items().stream() 42 | .map( 43 | itemRequest -> Item.builder().skuId(itemRequest.skuId()).name(itemRequest.name()).qty(itemRequest.qty()) 44 | .build() 45 | ).toList()) 46 | .build(); 47 | return flowEngine.makeFlowable(orderPreparation, OrderPreparationFlowType.DEFAULT, Map.of()) 48 | .map(ResponseEntity::ok); 49 | } 50 | 51 | @GetMapping("/{id}") 52 | public Mono> getOrderPreparation(@PathVariable final String id) { 53 | return orderPreparationRepository.findById(UUID.fromString(id)).map(ResponseEntity::ok) 54 | .defaultIfEmpty(ResponseEntity.notFound().build()); 55 | } 56 | 57 | @PostMapping("/{id}/start-preparation") 58 | public Mono> startPreparation(@PathVariable final String id) { 59 | return flowEngine.applyAction(UUID.fromString(id), 60 | OrderPreparationAction.START_PREPARATION, 61 | Map.of()) 62 | .map(ResponseEntity::ok).defaultIfEmpty(ResponseEntity.notFound().build()); 63 | } 64 | 65 | @PostMapping("/{id}/pick-items") 66 | public Mono> pickItems(@PathVariable final String id, 67 | @RequestBody final PickItemsRequest pickItemsRequest) { 68 | return flowEngine.applyAction(UUID.fromString(id), 69 | OrderPreparationAction.PICK_ITEMS, 70 | pickItemsRequest) 71 | .map(ResponseEntity::ok).defaultIfEmpty(ResponseEntity.notFound().build()); 72 | } 73 | 74 | @PostMapping("/{id}/pickup") 75 | public Mono> pickup(@PathVariable final String id) { 76 | return flowEngine.applyAction(UUID.fromString(id), 77 | OrderPreparationAction.PICKUP, 78 | Map.of()).map(ResponseEntity::ok) 79 | .defaultIfEmpty(ResponseEntity.notFound().build()); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/java/com/progmod/poc/infra/secondary/KafkaEventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.infra.secondary; 2 | 3 | import com.progmod.flow.domain.model.Flowable; 4 | import com.progmod.flow.domain.port.EventsPublisher; 5 | import lombok.extern.log4j.Log4j2; 6 | import org.springframework.stereotype.Component; 7 | import reactor.core.publisher.Mono; 8 | 9 | @Component 10 | @Log4j2 11 | public class KafkaEventPublisher implements EventsPublisher { 12 | @Override 13 | public Mono publishEvents(final Flowable flowable) { 14 | log.info("PUBLISH EVENTS TO KAFKA IF I WANT"); 15 | return Mono.empty(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | 4 | spring: 5 | r2dbc: 6 | url: r2dbc:postgresql://localhost:5432/postgres?currentSchema=poc 7 | username: admin 8 | password: admin 9 | 10 | flyway: 11 | enabled: true 12 | validate-on-migrate: true 13 | baseline-on-migrate: true 14 | baseline-version: 0.0 15 | locations: [ 'classpath:db/migration' ] 16 | url: jdbc:postgresql://localhost:5432/postgres 17 | schemas: [ 'poc' ] 18 | user: "admin" 19 | password: "admin" 20 | 21 | output: 22 | ansi: 23 | enabled: ALWAYS 24 | 25 | flow: 26 | actionLogger: 27 | enabled: true 28 | taskConsumer: 29 | scheduleMilli: 1000 30 | lockService: 31 | removeBlockedLockScheduleMilli: 10000 32 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/resources/db/migration/V1__Initial_version.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA IF NOT EXISTS poc AUTHORIZATION "admin"; 2 | 3 | CREATE TABLE IF NOT EXISTS poc.order_preparation ( 4 | id uuid NOT NULL, 5 | data jsonb NOT NULL, 6 | CONSTRAINT order_preparation_pk PRIMARY KEY (id) 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS poc.flow_task ( 10 | id varchar NOT NULL, 11 | score int8 NOT NULL, 12 | status varchar NOT NULL, 13 | ver int4 NOT NULL, 14 | CONSTRAINT flow_task_pk PRIMARY KEY (id) 15 | ); 16 | 17 | -------------------------------------------------------------------------------- /order-preparation-poc-reactive/src/main/resources/flow/in-store-workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "name": "START_PREPARATION", 5 | "delegate": "startPreparationDelegate" 6 | }, 7 | { 8 | "name": "PICK_ITEMS", 9 | "delegate": "pickItemsDelegate" 10 | }, 11 | { 12 | "name": "PICKUP", 13 | "delegate": "pickupDelegate" 14 | }, 15 | { 16 | "name": "NOTIFY_OM", 17 | "delegate": "notifyDelegate" 18 | } 19 | ], 20 | "states": [ 21 | { 22 | "name": "TO_PREPARE", 23 | "initial": true, 24 | "transitions": [ 25 | { 26 | "action": "START_PREPARATION", 27 | "result": { 28 | "success": "IN_PREPARATION" 29 | } 30 | } 31 | ] 32 | }, 33 | { 34 | "name": "IN_PREPARATION", 35 | "transitions": [ 36 | { 37 | "action": "PICK_ITEMS", 38 | "result": { 39 | "partial": "IN_PREPARATION", 40 | "full": "PENDING_PICKUP" 41 | } 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "PENDING_PICKUP", 47 | "transitions": [ 48 | { 49 | "action": "PICKUP", 50 | "result": { 51 | "success": "DELIVERED" 52 | } 53 | } 54 | ] 55 | }, 56 | { 57 | "name": "DELIVERED", 58 | "transitions": [ 59 | { 60 | "action": "NOTIFY_OM", 61 | "result": { 62 | "success": "COMPLETED" 63 | }, 64 | "exceptions": { 65 | "001": "RETRY_NOTIFICATION" 66 | }, 67 | "timer": { 68 | "sec": 0 69 | } 70 | } 71 | ] 72 | }, 73 | { 74 | "name": "RETRY_NOTIFICATION", 75 | "transitions": [ 76 | { 77 | "action": "NOTIFY_OM", 78 | "result": { 79 | "success": "COMPLETED" 80 | }, 81 | "exceptions": { 82 | "001": "RETRY_NOTIFICATION" 83 | }, 84 | "retry": { 85 | "number": 3, 86 | "exceeded": "ERROR" 87 | }, 88 | "timer": { 89 | "sec": 10 90 | } 91 | } 92 | ] 93 | }, 94 | { 95 | "name": "ERROR", 96 | "transitions": [] 97 | }, 98 | { 99 | "name": "COMPLETED", 100 | "transitions": [] 101 | } 102 | ] 103 | } -------------------------------------------------------------------------------- /order-preparation-poc/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | bin/ 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | out/ 24 | !**/src/main/**/out/ 25 | !**/src/test/**/out/ 26 | 27 | ### NetBeans ### 28 | /nbproject/private/ 29 | /nbbuild/ 30 | /dist/ 31 | /nbdist/ 32 | /.nb-gradle/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | /ui/node_modules/ 37 | /ui/dist/ 38 | -------------------------------------------------------------------------------- /order-preparation-poc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [Order Preparation POC](#order-preparation-poc) 6 | - [Use Case Explanation](#use-case-explanation) 7 | - [Process rules:](#process-rules) 8 | - [Aggregate rules:](#aggregate-rules) 9 | - [Note on Repository:](#note-on-repository) 10 | - [DataBase tables](#database-tables) 11 | - [Install and launch](#install-and-launch) 12 | - [APIs collection](#apis-collection) 13 | - [Postman collection](#postman-collection) 14 | - [Curl commands](#curl-commands) 15 | - [Example of a COMPLETE OrderPreparation](#example-of-a-complete-orderpreparation) 16 | 17 | 18 | 19 | 20 | # Order Preparation POC 21 | 22 | Here is a complete example of an Order Preparation domain. 23 | > The same application exist in reactive mode (spring webflux and project reactor) under 24 | > **order-preparation-poc-reactive** 25 | 26 | > Prerequisite: **JAVA 17** or higher 27 | 28 | ## Use Case Explanation 29 | 30 | Our application is designed to manage the preparation of orders in a click-and-collect retail environment.
31 | When a customer places an order online and chooses the click-and-collect option, the orderPreparation must be created in our system in a 'TO_PREPARE' state. 32 | The goal is to efficiently move the order through various steps until it's ready for the customer to pick up.
33 | The following states and actions define this journey: 34 | 35 | - States: 36 | - TO_PREPARE: initial state 37 | - IN_PREPARATION: The order is actively being prepared, items are being picked 38 | - PENDING_PICKUP: Preparation is complete, and the order is waiting for the customer to collect it 39 | - DELIVERED: The customer has picked up the order, completing the process 40 | - Actions: 41 | - START_PREPARATION : start the preparation,transitioning to IN_PREPARATION state 42 | - PICK_ITEMS : Select items for the order. This can occur multiple times. Once all items are picked, the order moves to 'PENDING_PICKUP'. 43 | - PICKUP : The customer collects their order, transitioning it to the 'DELIVERED' state. 44 | - NOTIFY_OM (notify Order Manager): Automatically triggered when the order reaches the 'DELIVERED' state, informing the Order Manager that the order has been picked up. 45 | 46 | ### Process rules: 47 | - Sequential Flow: The order must follow the sequence of 'START_PREPARATION' → 'PICK_ITEMS' → 'PICKUP'. 48 | - Multiple Item Picking: 'PICK_ITEMS' can be executed several times as needed. Only when all items are ready does the order shift to 'PENDING_PICKUP'. 49 | - Automatic Notification: Upon reaching 'DELIVERED', an automatic notification is sent to the Order Manager. 50 | - Error Handling: In case of notification failure, the order enters 'RETRY_NOTIFICATION', where it attempts to notify the Order Manager again every 10 seconds, up to 3 retries. Success moves the order to a 'COMPLETE' state, while failure leads to an 'ERROR' state. 51 | 52 | > check the **[in-store-workflow.json](src/main/resources/flow/in-store-workflow.json)** to understand how all these rules are configured 53 | 54 | ### Aggregate rules: 55 | 56 | - Item Existence Check: The system checks that the items being picked are part of the original order. 57 | - Quantity Verification: The quantity of each item picked cannot exceed the quantity specified in the order. 58 | 59 | > check **[OrderPreparation.java](src/main/java/com/progmod/poc/domain/OrderPreparation.java)** and **[Item.java](src/main/java/com/progmod/poc/domain/Item.java)** to understand how these business invariants are coded 60 | 61 | ## Note on Repository: 62 | 63 | The poc repository uses a built-in framework helper for using a postgres jsonb, where all the aggregate is saved in one 'data' column.
64 | check **[PocConfig.java](src/main/java/com/progmod/poc/PocConfig.java)** where we define the repository using the framework helper class 65 | ```java 66 | @Bean 67 | public FlowRepository orderPostgresRepository() { 68 | return new BasePostgresJsonRepository<>(OrderPreparation.class) 69 | .setTableInfo("order_preparation", "data"); 70 | } 71 | ``` 72 | > It is up to you to define your aggregate repository, the only prerequisite is to implements the **FlowRepository** 73 | 74 | ## DataBase tables 75 | 76 | we create 2 tables for this POC 77 | - The Aggregate Table 78 | ```roomsql 79 | CREATE TABLE IF NOT EXISTS poc.order_preparation ( 80 | id uuid NOT NULL, 81 | data jsonb NOT NULL, 82 | CONSTRAINT order_preparation_pk PRIMARY KEY (id) 83 | ); 84 | ``` 85 | 86 | - Task Table used by the framework to handle async system tasks 87 | 88 | ```roomsql 89 | CREATE TABLE IF NOT EXISTS poc.flow_task ( 90 | id varchar NOT NULL, 91 | score int8 NOT NULL, 92 | status varchar NOT NULL, 93 | ver int4 NOT NULL, 94 | CONSTRAINT flow_task_pk PRIMARY KEY (id) 95 | ); 96 | ``` 97 | 98 | ## Install and launch 99 | 100 | - **clone the repo** 101 | ```shell 102 | git clone https://github.com/progmodEK/flow-driven-domain.git 103 | ``` 104 | 105 | - **launch a postgres container** 106 | ```shell 107 | cd flow-driven-domain 108 | mkdir -p ./tools/data/postgres-data 109 | docker-compose up & 110 | ``` 111 | 112 | - **launch the application** 113 | ```shell 114 | ./gradlew :order-preparation-poc:bootRun 115 | ``` 116 | > replace with **order-preparation-poc-reactive** for the reactive app 117 | 118 | 119 | ## APIs collection 120 | 121 | After starting the application, you can interact with the API running on port 8081.
122 | 123 | ### Postman collection 124 | > if you are using Postman, import this collection **./tools/POC.postman_collection**
125 | > and just use it 126 | 127 | ### Curl commands 128 | > Replace 'ID' in the URL with the actual aggregate ID obtained after creating an order. 129 | 130 | - create an order to prepare 131 | ```shell 132 | curl 'http://localhost:8081/pocs' \ 133 | --header 'Content-Type: application/json' \ 134 | --data '{ 135 | "orderRef" : "orderXYZ", 136 | "items": [ 137 | { 138 | "skuId": "123", 139 | "name": "sku123", 140 | "qty": 2 141 | }, 142 | { 143 | "skuId": "456", 144 | "name": "sku456", 145 | "qty": 3 146 | } 147 | ] 148 | }' 149 | ``` 150 | 151 | - start the preparation 152 | ```shell 153 | curl --request POST 'http://localhost:8081/pocs/ID/start-preparation' 154 | ``` 155 | 156 | - pick items 157 | ```shell 158 | curl 'http://localhost:8081/pocs/ID/pick-items' \ 159 | --header 'Content-Type: application/json' \ 160 | --data '{ 161 | "pickItems" : [ 162 | { 163 | "skuId" : "123", 164 | "qty": 2 165 | }, 166 | { 167 | "skuId" : "456", 168 | "qty": 3 169 | } 170 | 171 | ] 172 | }' 173 | ``` 174 | 175 | - pickup the order 176 | ```shell 177 | curl --request POST 'http://localhost:8081/pocs/ID/pickup' 178 | ``` 179 | 180 | - view the Aggregate 181 | ```shell 182 | curl 'http://localhost:8081/pocs/ID' 183 | ``` 184 | 185 | > Testing Scenarios: 186 | >- Ensure you cant "pick items" before "start preparation" 187 | >- Verify you cant pick items more than the order's quantity 188 | >- Verify that you can call multiple times "pick items" (all the api with less quantities and you will stay in the state IN_PREPARATION 189 | >- check that after PICKUP invoked, there we will an async action invoked by the system to notify the Order manager (view the aggregate to check how it evolves) 190 | 191 | 192 | ## Example of a COMPLETE OrderPreparation 193 | 194 | ```json 195 | { 196 | "id": "17e0deab-bcc3-43e0-a175-d6c0a1eb11fa", 197 | "orderRef": "orderXYZ", 198 | "items": [ 199 | { 200 | "skuId": "123", 201 | "name": "sku123", 202 | "qty": 2, 203 | "qtyPrepared": 2, 204 | "pickedUp": true 205 | }, 206 | { 207 | "skuId": "456", 208 | "name": "sku456", 209 | "qty": 3, 210 | "qtyPrepared": 3, 211 | "pickedUp": true 212 | } 213 | ], 214 | "state": "COMPLETED", 215 | "flow": { 216 | "expiresAt": null, 217 | "actions": [ 218 | { 219 | "name": "START_PREPARATION", 220 | "type": "USER", 221 | "count": 2, 222 | "variables": { 223 | "transition": "success" 224 | }, 225 | "executions": [ 226 | { 227 | "executedAt": "2024-01-07T08:51:47.754002Z", 228 | "result": "success", 229 | "error": null, 230 | "fromState": "TO_PREPARE", 231 | "toState": "IN_PREPARATION" 232 | }, 233 | { 234 | "executedAt": "2024-01-07T08:51:49.766903Z", 235 | "result": "error", 236 | "error": "InvalidActionException: Desired action 'START_PREPARATION' does not match current flow rules", 237 | "fromState": "IN_PREPARATION", 238 | "toState": "IN_PREPARATION" 239 | } 240 | ] 241 | }, 242 | { 243 | "name": "PICK_ITEMS", 244 | "type": "USER", 245 | "count": 1, 246 | "variables": { 247 | "transition": "full" 248 | }, 249 | "executions": [ 250 | { 251 | "executedAt": "2024-01-07T08:51:52.109008Z", 252 | "result": "success", 253 | "error": null, 254 | "fromState": "IN_PREPARATION", 255 | "toState": "PENDING_PICKUP" 256 | } 257 | ] 258 | }, 259 | { 260 | "name": "PICKUP", 261 | "type": "USER", 262 | "count": 1, 263 | "variables": { 264 | "transition": "success" 265 | }, 266 | "executions": [ 267 | { 268 | "executedAt": "2024-01-07T08:51:54.463242Z", 269 | "result": "success", 270 | "error": null, 271 | "fromState": "PENDING_PICKUP", 272 | "toState": "DELIVERED" 273 | } 274 | ] 275 | }, 276 | { 277 | "name": "NOTIFY_OM", 278 | "type": "SYSTEM", 279 | "count": 2, 280 | "variables": { 281 | "transition": "success" 282 | }, 283 | "executions": [ 284 | { 285 | "executedAt": "2024-01-07T08:51:54.572989Z", 286 | "result": "error", 287 | "error": "DelegateException: notification error", 288 | "fromState": "DELIVERED", 289 | "toState": "RETRY_NOTIFICATION" 290 | }, 291 | { 292 | "executedAt": "2024-01-07T08:52:04.697400Z", 293 | "result": "success", 294 | "error": null, 295 | "fromState": "RETRY_NOTIFICATION", 296 | "toState": "COMPLETED" 297 | } 298 | ] 299 | } 300 | ], 301 | "flowType": "DEFAULT", 302 | "eligibleActions": [], 303 | "variables": {} 304 | } 305 | } 306 | ``` 307 | 308 | > Note that action that generates error are also traced,
309 | > for ex: START_PREPARATION was called twice, the second time it generates an error cause the order was already in IN_PREPARATION state 310 | ```json 311 | { 312 | "name": "START_PREPARATION", 313 | "type": "USER", 314 | "count": 2, 315 | "variables": { 316 | "transition": "success" 317 | }, 318 | "executions": [ 319 | { 320 | "executedAt": "2024-01-07T08:51:47.754002Z", 321 | "result": "success", 322 | "error": null, 323 | "fromState": "TO_PREPARE", 324 | "toState": "IN_PREPARATION" 325 | }, 326 | { 327 | "executedAt": "2024-01-07T08:51:49.766903Z", 328 | "result": "error", 329 | "error": "InvalidActionException: Desired action 'START_PREPARATION' does not match current flow rules", 330 | "fromState": "IN_PREPARATION", 331 | "toState": "IN_PREPARATION" 332 | } 333 | ] 334 | } 335 | ``` 336 | 337 | > Note also that NOTIFY_OM (the async automatic system action) was executed twice, first time was in error, the second time it transit to COMPLETED 338 | ```json 339 | { 340 | "name": "NOTIFY_OM", 341 | "type": "SYSTEM", 342 | "count": 2, 343 | "variables": { 344 | "transition": "success" 345 | }, 346 | "executions": [ 347 | { 348 | "executedAt": "2024-01-07T08:51:54.572989Z", 349 | "result": "error", 350 | "error": "DelegateException: notification error", 351 | "fromState": "DELIVERED", 352 | "toState": "RETRY_NOTIFICATION" 353 | }, 354 | { 355 | "executedAt": "2024-01-07T08:52:04.697400Z", 356 | "result": "success", 357 | "error": null, 358 | "fromState": "RETRY_NOTIFICATION", 359 | "toState": "COMPLETED" 360 | } 361 | ] 362 | } 363 | ``` -------------------------------------------------------------------------------- /order-preparation-poc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "io.freefair.lombok" version "8.4" 4 | id "org.springframework.boot" version "3.1.2" 5 | id "io.spring.dependency-management" version "1.1.4" 6 | } 7 | 8 | group "com.progmod.poc" 9 | version "0.0.1-SNAPSHOT" 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | // Flow Driven Domain Library 17 | implementation "io.github.progmodek:flow:1.0.1" 18 | 19 | //Spring 20 | implementation "org.springframework.boot:spring-boot-starter-web" 21 | // Postgres 22 | implementation "org.postgresql:postgresql" 23 | 24 | //flyway 25 | runtimeOnly "org.flywaydb:flyway-core" 26 | 27 | } 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/PocApplication.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | //@EnableTransactionManagement 10 | public class PocApplication { 11 | 12 | public static void main(final String[] args) { 13 | SpringApplication.run(PocApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/PocConfig.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc; 2 | 3 | import com.progmod.flow.domain.port.FlowRepository; 4 | import com.progmod.flow.domain.service.FlowEngine; 5 | import com.progmod.flow.infra.postgres.BasePostgresJsonRepository; 6 | import com.progmod.poc.domain.OrderPreparation; 7 | import com.progmod.poc.domain.flow.OrderPreparationFlowType; 8 | import java.util.UUID; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class PocConfig { 14 | 15 | @Bean 16 | public FlowRepository orderPostgresRepository() { 17 | return new BasePostgresJsonRepository<>(OrderPreparation.class) 18 | .setTableInfo("order_preparation", "data"); 19 | } 20 | 21 | @Bean 22 | FlowEngine flowEngine(final FlowRepository orderPostgresRepository) { 23 | return new FlowEngine<>(UUID.class, OrderPreparationFlowType.class, orderPostgresRepository); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/Item.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Builder 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class Item { 13 | String skuId; 14 | String name; 15 | int qty; 16 | int qtyPrepared; 17 | boolean pickedUp; 18 | 19 | 20 | /** 21 | * Update Prepared Qty and invariants. 22 | */ 23 | public Item updatePreparedQty(final int qtyPrepared) { 24 | if (qtyPrepared > this.qty) { 25 | throw new IllegalArgumentException("prepared qty is greater than order qty"); 26 | } 27 | this.qtyPrepared = qtyPrepared; 28 | if (qty == qtyPrepared) { 29 | pickedUp = true; 30 | } 31 | return this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/OrderPreparation.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 4 | import com.progmod.flow.domain.model.Flow; 5 | import com.progmod.flow.domain.model.Flowable; 6 | import com.progmod.poc.dto.PickItemRequest; 7 | import com.progmod.poc.dto.PickItemsRequest; 8 | import java.beans.Transient; 9 | import java.util.List; 10 | import java.util.UUID; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | @JsonPropertyOrder({"id", "orderRef", "items", "state", "flow"}) 21 | public class OrderPreparation implements Flowable { 22 | @Builder.Default 23 | private UUID id = UUID.randomUUID(); 24 | private String orderRef; 25 | private List items; 26 | 27 | private String state; 28 | private Flow flow; 29 | 30 | 31 | /** 32 | * Update Preparation and check Aggregate invariants. 33 | */ 34 | public void updatePreparation(final PickItemsRequest pickItemsRequest) { 35 | // check picked items exist 36 | final List pickedSkus = pickItemsRequest.pickItems().stream() 37 | .map(PickItemRequest::skuId) 38 | .toList(); 39 | final List existingSkus = items.stream() 40 | .map(Item::getSkuId) 41 | .toList(); 42 | final boolean allExists = pickedSkus.stream().allMatch(s -> existingSkus.contains(s)); 43 | if (!allExists) { 44 | throw new IllegalArgumentException("prepared items does not exists"); 45 | } 46 | 47 | // update Items preparation 48 | pickItemsRequest.pickItems().stream() 49 | .forEach( 50 | pickItemRequest -> items.stream().filter(item -> item.skuId.equals(pickItemRequest.skuId())).findFirst() 51 | .map(item -> item.updatePreparedQty(pickItemRequest.qty())) 52 | ); 53 | } 54 | 55 | @Transient 56 | public boolean isFullyPrepared() { 57 | return items.stream() 58 | .filter(item -> item.qty == item.qtyPrepared) 59 | .toList().size() == items.size(); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/delegate/PickItemsDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import static com.progmod.flow.utils.FlowUtils.ACTION_TRANSITION_VARIABLE; 4 | 5 | import com.progmod.flow.domain.service.delegate.ActionDelegate; 6 | import com.progmod.poc.domain.OrderPreparation; 7 | import com.progmod.poc.dto.PickItemsRequest; 8 | import java.util.Map; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.log4j.Log4j2; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | @RequiredArgsConstructor 15 | @Log4j2 16 | public class PickItemsDelegate implements ActionDelegate { 17 | 18 | @Override 19 | public OrderPreparation execute(final OrderPreparation orderPreparation, 20 | final Map actionContext, 21 | final PickItemsRequest pickItemsRequest) { 22 | orderPreparation.updatePreparation(pickItemsRequest); 23 | actionContext.put(ACTION_TRANSITION_VARIABLE, orderPreparation.isFullyPrepared() ? "full" : "partial"); 24 | return orderPreparation; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/delegate/PickupDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import com.progmod.flow.domain.service.delegate.ActionDelegate; 4 | import com.progmod.poc.domain.OrderPreparation; 5 | import java.util.Map; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @RequiredArgsConstructor 12 | @Log4j2 13 | public class PickupDelegate implements ActionDelegate, OrderPreparation> { 14 | 15 | 16 | @Override 17 | public OrderPreparation execute(final OrderPreparation orderPreparation, final Map variables, 18 | final Map inputParams) { 19 | log.info("pickup invoked"); 20 | return orderPreparation; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/delegate/notifyDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import com.progmod.flow.domain.service.DelegateException; 4 | import com.progmod.flow.domain.service.delegate.SystemActionDelegate; 5 | import com.progmod.poc.domain.OrderPreparation; 6 | import java.util.Map; 7 | import java.util.Random; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | @RequiredArgsConstructor 14 | @Log4j2 15 | public class notifyDelegate extends SystemActionDelegate { 16 | 17 | 18 | @Override 19 | public OrderPreparation execute(final OrderPreparation orderPreparation, final Map variables) { 20 | log.info("notification invoked"); 21 | // simulate a 50% error to test the flow 22 | if ((new Random()).nextBoolean()) { 23 | throw new DelegateException(1, "notification error"); 24 | } else { 25 | return orderPreparation; 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/delegate/startPreparationDelegate.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.delegate; 2 | 3 | import com.progmod.flow.domain.service.delegate.ActionDelegate; 4 | import com.progmod.poc.domain.OrderPreparation; 5 | import java.util.Map; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @RequiredArgsConstructor 12 | @Log4j2 13 | public class startPreparationDelegate 14 | implements ActionDelegate, OrderPreparation> { 15 | 16 | @Override 17 | public OrderPreparation execute(final OrderPreparation orderPreparation, 18 | final Map actionContext, 19 | final Map inputParams) { 20 | log.info("startPreparation invoked"); 21 | return orderPreparation; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/flow/OrderPreparationAction.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.flow; 2 | 3 | import static com.progmod.flow.domain.model.ActionType.SYSTEM; 4 | import static com.progmod.flow.domain.model.ActionType.USER; 5 | 6 | import com.progmod.flow.domain.model.ActionType; 7 | import com.progmod.flow.domain.service.parser.definition.FlowAction; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | /** 12 | * Enumeration declaring all available actions name inside a flow. 13 | */ 14 | @Getter 15 | @RequiredArgsConstructor 16 | public enum OrderPreparationAction implements FlowAction { 17 | START_PREPARATION(USER), 18 | PICK_ITEMS(USER), 19 | PICKUP(USER), 20 | NOTIFY_OM(SYSTEM); 21 | 22 | private final ActionType type; 23 | } 24 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/flow/OrderPreparationFlowType.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.flow; 2 | 3 | import com.progmod.flow.domain.service.parser.definition.FlowAction; 4 | import com.progmod.flow.domain.service.parser.definition.FlowState; 5 | import com.progmod.flow.domain.service.parser.definition.FlowType; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | 9 | @AllArgsConstructor 10 | public enum OrderPreparationFlowType implements FlowType { 11 | DEFAULT("flow/in-store-workflow.json", OrderPreparationAction.class, OrderPreparationState.class); 12 | 13 | @Getter 14 | private final String template; 15 | @Getter 16 | private final Class flowActionType; 17 | @Getter 18 | private final Class flowStateType; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/domain/flow/OrderPreparationState.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.domain.flow; 2 | 3 | import com.progmod.flow.domain.service.parser.definition.FlowState; 4 | import lombok.Getter; 5 | 6 | /** 7 | * Enumeration declaring all available states for the flow. 8 | */ 9 | @Getter 10 | public enum OrderPreparationState implements FlowState { 11 | TO_PREPARE, 12 | IN_PREPARATION, 13 | PENDING_PICKUP, 14 | DELIVERED, 15 | RETRY_NOTIFICATION, 16 | COMPLETED, 17 | ERROR 18 | } 19 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/dto/CreateOrderPreparationRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public record CreateOrderPreparationRequest(@JsonProperty("orderRef") String orderRef, 7 | @JsonProperty("items") List items) { 8 | } 9 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/dto/ItemRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record ItemRequest(@JsonProperty("skuId") String skuId, @JsonProperty("name") String name, 6 | @JsonProperty("qty") int qty) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/dto/PickItemRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record PickItemRequest(@JsonProperty("skuId") String skuId, @JsonProperty("qty") int qty) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/dto/PickItemsRequest.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public record PickItemsRequest(@JsonProperty("pickItems") List pickItems) { 7 | } 8 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/infra/primary/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.infra.primary; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.springframework.http.HttpStatusCode; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | 9 | /** 10 | * Class defined to manage all controller exceptions, error response and status 11 | */ 12 | @Log4j2 13 | @ControllerAdvice 14 | public class ErrorHandler { 15 | 16 | //////////////// 17 | // 4xx errors // 18 | //////////////// 19 | @ExceptionHandler 20 | ResponseEntity handle(final RuntimeException exception) { 21 | return ResponseEntity.status(HttpStatusCode.valueOf(406)).body(exception.getMessage()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/infra/primary/PocController.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.infra.primary; 2 | 3 | import com.progmod.flow.domain.port.FlowRepository; 4 | import com.progmod.flow.domain.service.FlowEngine; 5 | import com.progmod.poc.domain.Item; 6 | import com.progmod.poc.domain.OrderPreparation; 7 | import com.progmod.poc.domain.flow.OrderPreparationAction; 8 | import com.progmod.poc.domain.flow.OrderPreparationFlowType; 9 | import com.progmod.poc.dto.CreateOrderPreparationRequest; 10 | import com.progmod.poc.dto.PickItemsRequest; 11 | import java.util.Map; 12 | import java.util.UUID; 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.extern.log4j.Log4j2; 15 | import org.springframework.context.ApplicationContext; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.PostMapping; 20 | import org.springframework.web.bind.annotation.RequestBody; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | @RestController 25 | @Log4j2 26 | @RequestMapping("/pocs") 27 | @RequiredArgsConstructor 28 | public class PocController { 29 | 30 | protected final FlowRepository orderPreparationRepository; 31 | protected final FlowEngine flowEngine; 32 | private ApplicationContext applicaioncontext; 33 | 34 | @PostMapping 35 | public ResponseEntity createOrderPreparation( 36 | @RequestBody final CreateOrderPreparationRequest createProductProcessRequest) { 37 | 38 | final OrderPreparation orderPreparation = OrderPreparation.builder() 39 | .orderRef(createProductProcessRequest.orderRef()) 40 | .items(createProductProcessRequest.items().stream() 41 | .map( 42 | itemRequest -> Item.builder().skuId(itemRequest.skuId()).name(itemRequest.name()).qty(itemRequest.qty()) 43 | .build() 44 | ).toList()) 45 | .build(); 46 | return 47 | ResponseEntity.ok(flowEngine.makeFlowable(orderPreparation, OrderPreparationFlowType.DEFAULT, Map.of())); 48 | } 49 | 50 | @GetMapping("/{id}") 51 | public ResponseEntity getOrderPreparation(@PathVariable final String id) { 52 | return ResponseEntity.of(orderPreparationRepository.findById(UUID.fromString(id))); 53 | } 54 | 55 | @PostMapping("/{id}/start-preparation") 56 | public ResponseEntity startPreparation(@PathVariable final String id) { 57 | return 58 | ResponseEntity.ok(flowEngine.applyAction(UUID.fromString(id), 59 | OrderPreparationAction.START_PREPARATION, 60 | Map.of())); 61 | } 62 | 63 | @PostMapping("/{id}/pick-items") 64 | public ResponseEntity pickItems(@PathVariable final String id, 65 | @RequestBody final PickItemsRequest pickItemsRequest) { 66 | return 67 | ResponseEntity.ok(flowEngine.applyAction(UUID.fromString(id), 68 | OrderPreparationAction.PICK_ITEMS, 69 | pickItemsRequest)); 70 | 71 | } 72 | 73 | @PostMapping("/{id}/pickup") 74 | //@Transactional(propagation = Propagation.REQUIRED) 75 | public ResponseEntity pickup(@PathVariable final String id) { 76 | return 77 | ResponseEntity.ok( flowEngine.applyAction(UUID.fromString(id), 78 | OrderPreparationAction.PICKUP, 79 | Map.of())); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/java/com/progmod/poc/infra/secondary/KafkaEventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.progmod.poc.infra.secondary; 2 | 3 | import com.progmod.flow.domain.model.Flowable; 4 | import com.progmod.flow.domain.port.EventsPublisher; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @Slf4j 10 | public class KafkaEventPublisher implements EventsPublisher { 11 | @Override 12 | public void publishEvents(final Flowable flow) { 13 | log.info("PUBLISH EVENTS TO KAFKA IF I WANT"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | 4 | spring: 5 | datasource: 6 | url: jdbc:postgresql://localhost:5432/postgres?currentSchema=poc 7 | username: admin 8 | password: admin 9 | 10 | flyway: 11 | enabled: true 12 | validate-on-migrate: true 13 | baseline-on-migrate: true 14 | baseline-version: 0.0 15 | locations: [ 'classpath:db/migration' ] 16 | url: jdbc:postgresql://localhost:5432/postgres 17 | schemas: [ 'poc' ] 18 | user: "admin" 19 | password: "admin" 20 | 21 | output: 22 | ansi: 23 | enabled: ALWAYS 24 | 25 | flow: 26 | actionLogger: 27 | enabled: true 28 | taskConsumer: 29 | scheduleMilli: 1000 30 | lockService: 31 | removeBlockedLockScheduleMilli: 10000 32 | 33 | 34 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/resources/db/migration/V1__Initial_version.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA IF NOT EXISTS poc AUTHORIZATION "admin"; 2 | 3 | CREATE TABLE IF NOT EXISTS poc.order_preparation ( 4 | id uuid NOT NULL, 5 | data jsonb NOT NULL, 6 | CONSTRAINT order_preparation_pk PRIMARY KEY (id) 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS poc.flow_task ( 10 | id varchar NOT NULL, 11 | score int8 NOT NULL, 12 | status varchar NOT NULL, 13 | ver int4 NOT NULL, 14 | CONSTRAINT flow_task_pk PRIMARY KEY (id) 15 | ); 16 | 17 | -------------------------------------------------------------------------------- /order-preparation-poc/src/main/resources/flow/in-store-workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "name": "START_PREPARATION", 5 | "delegate": "startPreparationDelegate" 6 | }, 7 | { 8 | "name": "PICK_ITEMS", 9 | "delegate": "pickItemsDelegate" 10 | }, 11 | { 12 | "name": "PICKUP", 13 | "delegate": "pickupDelegate" 14 | }, 15 | { 16 | "name": "NOTIFY_OM", 17 | "delegate": "notifyDelegate" 18 | } 19 | ], 20 | "states": [ 21 | { 22 | "name": "TO_PREPARE", 23 | "initial": true, 24 | "transitions": [ 25 | { 26 | "action": "START_PREPARATION", 27 | "result": { 28 | "success": "IN_PREPARATION" 29 | } 30 | } 31 | ] 32 | }, 33 | { 34 | "name": "IN_PREPARATION", 35 | "transitions": [ 36 | { 37 | "action": "PICK_ITEMS", 38 | "result": { 39 | "partial": "IN_PREPARATION", 40 | "full": "PENDING_PICKUP" 41 | } 42 | } 43 | ] 44 | }, 45 | { 46 | "name": "PENDING_PICKUP", 47 | "transitions": [ 48 | { 49 | "action": "PICKUP", 50 | "result": { 51 | "success": "DELIVERED" 52 | } 53 | } 54 | ] 55 | }, 56 | { 57 | "name": "DELIVERED", 58 | "transitions": [ 59 | { 60 | "action": "NOTIFY_OM", 61 | "result": { 62 | "success": "COMPLETED" 63 | }, 64 | "exceptions": { 65 | "001": "RETRY_NOTIFICATION" 66 | }, 67 | "timer": { 68 | "sec": 0 69 | } 70 | } 71 | ] 72 | }, 73 | { 74 | "name": "RETRY_NOTIFICATION", 75 | "transitions": [ 76 | { 77 | "action": "NOTIFY_OM", 78 | "result": { 79 | "success": "COMPLETED" 80 | }, 81 | "exceptions": { 82 | "001": "RETRY_NOTIFICATION" 83 | }, 84 | "retry": { 85 | "number": 3, 86 | "exceeded": "ERROR" 87 | }, 88 | "timer": { 89 | "sec": 10 90 | } 91 | } 92 | ] 93 | }, 94 | { 95 | "name": "ERROR", 96 | "transitions": [] 97 | }, 98 | { 99 | "name": "COMPLETED", 100 | "transitions": [] 101 | } 102 | ] 103 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='fdd-poc' 2 | include 'order-preparation-poc' 3 | include 'order-preparation-poc-reactive' 4 | 5 | 6 | -------------------------------------------------------------------------------- /tools/POC.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "84adc205-756a-4275-883f-72a906fcbabc", 4 | "name": "Flow Order Preparation POC", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "29753349" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Create order preparation", 11 | "event": [ 12 | { 13 | "listen": "test", 14 | "script": { 15 | "exec": [ 16 | "postman.setEnvironmentVariable(\"flow_id\", pm.response.json().id);" 17 | ], 18 | "type": "text/javascript" 19 | } 20 | } 21 | ], 22 | "request": { 23 | "method": "POST", 24 | "header": [], 25 | "body": { 26 | "mode": "raw", 27 | "raw": "{\n \"orderRef\" : \"orderXYZ\",\n \"items\": [\n {\n \"skuId\": \"123\",\n \"name\": \"sku123\",\n \"qty\": 2\n },\n {\n \"skuId\": \"456\",\n \"name\": \"sku456\",\n \"qty\": 3\n }\n ]\n}", 28 | "options": { 29 | "raw": { 30 | "language": "json" 31 | } 32 | } 33 | }, 34 | "url": { 35 | "raw": "http://localhost:8081/pocs", 36 | "protocol": "http", 37 | "host": [ 38 | "localhost" 39 | ], 40 | "port": "8081", 41 | "path": [ 42 | "pocs" 43 | ] 44 | } 45 | }, 46 | "response": [] 47 | }, 48 | { 49 | "name": "Start Preparation", 50 | "request": { 51 | "method": "POST", 52 | "header": [], 53 | "body": { 54 | "mode": "raw", 55 | "raw": "{\n\n}\n", 56 | "options": { 57 | "raw": { 58 | "language": "json" 59 | } 60 | } 61 | }, 62 | "url": { 63 | "raw": "http://localhost:8081/pocs/{{flow_id}}/start-preparation", 64 | "protocol": "http", 65 | "host": [ 66 | "localhost" 67 | ], 68 | "port": "8081", 69 | "path": [ 70 | "pocs", 71 | "{{flow_id}}", 72 | "start-preparation" 73 | ] 74 | } 75 | }, 76 | "response": [] 77 | }, 78 | { 79 | "name": "Pick Items", 80 | "request": { 81 | "method": "POST", 82 | "header": [], 83 | "body": { 84 | "mode": "raw", 85 | "raw": "{\n \"pickItems\" : [\n {\n \"skuId\" : \"123\",\n \"qty\": 2\n },\n {\n \"skuId\" : \"456\",\n \"qty\": 3\n }\n\n ]\n}", 86 | "options": { 87 | "raw": { 88 | "language": "json" 89 | } 90 | } 91 | }, 92 | "url": { 93 | "raw": "http://localhost:8081/pocs/{{flow_id}}/pick-items", 94 | "protocol": "http", 95 | "host": [ 96 | "localhost" 97 | ], 98 | "port": "8081", 99 | "path": [ 100 | "pocs", 101 | "{{flow_id}}", 102 | "pick-items" 103 | ] 104 | } 105 | }, 106 | "response": [] 107 | }, 108 | { 109 | "name": "Pickup", 110 | "request": { 111 | "method": "POST", 112 | "header": [], 113 | "body": { 114 | "mode": "raw", 115 | "raw": "{\n\n}\n", 116 | "options": { 117 | "raw": { 118 | "language": "json" 119 | } 120 | } 121 | }, 122 | "url": { 123 | "raw": "http://localhost:8081/pocs/{{flow_id}}/pickup", 124 | "protocol": "http", 125 | "host": [ 126 | "localhost" 127 | ], 128 | "port": "8081", 129 | "path": [ 130 | "pocs", 131 | "{{flow_id}}", 132 | "pickup" 133 | ] 134 | } 135 | }, 136 | "response": [] 137 | }, 138 | { 139 | "name": "Get Process by Id", 140 | "protocolProfileBehavior": { 141 | "disableBodyPruning": true 142 | }, 143 | "request": { 144 | "method": "GET", 145 | "header": [], 146 | "body": { 147 | "mode": "raw", 148 | "raw": "" 149 | }, 150 | "url": { 151 | "raw": "http://localhost:8081/pocs/{{flow_id}}", 152 | "protocol": "http", 153 | "host": [ 154 | "localhost" 155 | ], 156 | "port": "8081", 157 | "path": [ 158 | "pocs", 159 | "{{flow_id}}" 160 | ] 161 | } 162 | }, 163 | "response": [] 164 | }, 165 | { 166 | "name": "Get All Processes", 167 | "protocolProfileBehavior": { 168 | "disableBodyPruning": true 169 | }, 170 | "request": { 171 | "method": "GET", 172 | "header": [], 173 | "body": { 174 | "mode": "raw", 175 | "raw": "" 176 | }, 177 | "url": { 178 | "raw": "http://localhost:8081/pocs/", 179 | "protocol": "http", 180 | "host": [ 181 | "localhost" 182 | ], 183 | "port": "8081", 184 | "path": [ 185 | "pocs", 186 | "" 187 | ] 188 | } 189 | }, 190 | "response": [] 191 | }, 192 | { 193 | "name": "get Workflow", 194 | "protocolProfileBehavior": { 195 | "disableBodyPruning": true 196 | }, 197 | "request": { 198 | "method": "GET", 199 | "header": [], 200 | "body": { 201 | "mode": "raw", 202 | "raw": "{\n \"productId\" : \"product1\"\n}" 203 | }, 204 | "url": { 205 | "raw": "http://localhost:8081/pocs/", 206 | "protocol": "http", 207 | "host": [ 208 | "localhost" 209 | ], 210 | "port": "8081", 211 | "path": [ 212 | "pocs", 213 | "" 214 | ] 215 | } 216 | }, 217 | "response": [] 218 | }, 219 | { 220 | "name": "get Workflow By type", 221 | "protocolProfileBehavior": { 222 | "disableBodyPruning": true 223 | }, 224 | "request": { 225 | "method": "GET", 226 | "header": [], 227 | "body": { 228 | "mode": "raw", 229 | "raw": "{\n \"productId\" : \"product1\"\n}" 230 | }, 231 | "url": { 232 | "raw": "http://localhost:8081/pocs/", 233 | "protocol": "http", 234 | "host": [ 235 | "localhost" 236 | ], 237 | "port": "8081", 238 | "path": [ 239 | "pocs", 240 | "" 241 | ] 242 | } 243 | }, 244 | "response": [] 245 | } 246 | ] 247 | } -------------------------------------------------------------------------------- /tools/checkStyle/sun_checks.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 93 | 94 | 95 | 97 | 98 | 99 | 102 | 103 | 104 | 106 | 108 | 109 | 110 | 111 | 112 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 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 | 167 | 168 | 169 | 170 | 172 | 174 | 175 | 176 | 178 | 179 | 180 | 181 | 183 | 184 | 185 | 186 | 188 | 189 | 190 | 191 | 193 | 194 | 195 | 196 | 198 | 199 | 200 | 201 | 203 | 204 | 205 | 206 | 208 | 209 | 210 | 211 | 213 | 214 | 215 | 216 | 218 | 219 | 220 | 221 | 223 | 224 | 225 | 226 | 227 | 229 | 231 | 233 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 264 | 265 | 266 | 269 | 270 | 271 | 272 | 278 | 279 | 280 | 281 | 284 | 285 | 286 | 287 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 302 | 303 | 304 | 305 | 306 | 307 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 322 | 323 | 324 | 325 | 328 | 329 | 330 | 331 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | --------------------------------------------------------------------------------