flow-driven-domain

Table of Contents generated with DocToc

Order Preparation POC

Here is a complete example of an Order Preparation domain.

The same application exist in reactive mode (spring webflux and project reactor) under order-preparation-poc-reactive

Prerequisite: JAVA 17 or higher

Use Case Explanation

Our application is designed to manage the preparation of orders in a click-and-collect retail environment.
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. The goal is to efficiently move the order through various steps until it’s ready for the customer to pick up.
The following states and actions define this journey:

Process rules:

check the in-store-workflow.json to understand how all these rules are configured

Aggregate rules:

check OrderPreparation.java and Item.java to understand how these business invariants are coded

Note on Repository:

The poc repository uses a built-in framework helper for using a postgres jsonb, where all the aggregate is saved in one ‘data’ column.
check PocConfig.java where we define the repository using the framework helper class

@Bean
public FlowRepository<OrderPreparation, UUID> orderPostgresRepository() {
  return new BasePostgresJsonRepository<>(OrderPreparation.class)
        .setTableInfo("order_preparation", "data");
}

It is up to you to define your aggregate repository, the only prerequisite is to implements the FlowRepository

DataBase tables

we create 2 tables for this POC

CREATE TABLE IF NOT EXISTS poc.flow_task (
	id varchar NOT NULL,
	score int8 NOT NULL,
	status varchar NOT NULL,
	ver int4 NOT NULL,
	CONSTRAINT flow_task_pk PRIMARY KEY (id)
);

Install and launch

APIs collection

After starting the application, you can interact with the API running on port 8081.

Postman collection

if you are using Postman, import this collection ./tools/POC.postman_collection
and just use it

Curl commands

Replace ‘ID’ in the URL with the actual aggregate ID obtained after creating an order.

Testing Scenarios:

Example of a COMPLETE OrderPreparation

{
  "id": "17e0deab-bcc3-43e0-a175-d6c0a1eb11fa",
  "orderRef": "orderXYZ",
  "items": [
    {
      "skuId": "123",
      "name": "sku123",
      "qty": 2,
      "qtyPrepared": 2,
      "pickedUp": true
    },
    {
      "skuId": "456",
      "name": "sku456",
      "qty": 3,
      "qtyPrepared": 3,
      "pickedUp": true
    }
  ],
  "state": "COMPLETED",
  "flow": {
    "expiresAt": null,
    "actions": [
      {
        "name": "START_PREPARATION",
        "type": "USER",
        "count": 2,
        "variables": {
          "transition": "success"
        },
        "executions": [
          {
            "executedAt": "2024-01-07T08:51:47.754002Z",
            "result": "success",
            "error": null,
            "fromState": "TO_PREPARE",
            "toState": "IN_PREPARATION"
          },
          {
            "executedAt": "2024-01-07T08:51:49.766903Z",
            "result": "error",
            "error": "InvalidActionException: Desired action 'START_PREPARATION' does not match current flow rules",
            "fromState": "IN_PREPARATION",
            "toState": "IN_PREPARATION"
          }
        ]
      },
      {
        "name": "PICK_ITEMS",
        "type": "USER",
        "count": 1,
        "variables": {
          "transition": "full"
        },
        "executions": [
          {
            "executedAt": "2024-01-07T08:51:52.109008Z",
            "result": "success",
            "error": null,
            "fromState": "IN_PREPARATION",
            "toState": "PENDING_PICKUP"
          }
        ]
      },
      {
        "name": "PICKUP",
        "type": "USER",
        "count": 1,
        "variables": {
          "transition": "success"
        },
        "executions": [
          {
            "executedAt": "2024-01-07T08:51:54.463242Z",
            "result": "success",
            "error": null,
            "fromState": "PENDING_PICKUP",
            "toState": "DELIVERED"
          }
        ]
      },
      {
        "name": "NOTIFY_OM",
        "type": "SYSTEM",
        "count": 2,
        "variables": {
          "transition": "success"
        },
        "executions": [
          {
            "executedAt": "2024-01-07T08:51:54.572989Z",
            "result": "error",
            "error": "DelegateException: notification error",
            "fromState": "DELIVERED",
            "toState": "RETRY_NOTIFICATION"
          },
          {
            "executedAt": "2024-01-07T08:52:04.697400Z",
            "result": "success",
            "error": null,
            "fromState": "RETRY_NOTIFICATION",
            "toState": "COMPLETED"
          }
        ]
      }
    ],
    "flowType": "DEFAULT",
    "eligibleActions": [],
    "variables": {}
  }
}

Note that action that generates error are also traced,
for ex: START_PREPARATION was called twice, the second time it generates an error cause the order was already in IN_PREPARATION state

      {
  "name": "START_PREPARATION",
  "type": "USER",
  "count": 2,
  "variables": {
    "transition": "success"
  },
  "executions": [
    {
      "executedAt": "2024-01-07T08:51:47.754002Z",
      "result": "success",
      "error": null,
      "fromState": "TO_PREPARE",
      "toState": "IN_PREPARATION"
    },
    {
      "executedAt": "2024-01-07T08:51:49.766903Z",
      "result": "error",
      "error": "InvalidActionException: Desired action 'START_PREPARATION' does not match current flow rules",
      "fromState": "IN_PREPARATION",
      "toState": "IN_PREPARATION"
    }
  ]
}

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

{
  "name": "NOTIFY_OM",
  "type": "SYSTEM",
  "count": 2,
  "variables": {
    "transition": "success"
  },
  "executions": [
    {
      "executedAt": "2024-01-07T08:51:54.572989Z",
      "result": "error",
      "error": "DelegateException: notification error",
      "fromState": "DELIVERED",
      "toState": "RETRY_NOTIFICATION"
    },
    {
      "executedAt": "2024-01-07T08:52:04.697400Z",
      "result": "success",
      "error": null,
      "fromState": "RETRY_NOTIFICATION",
      "toState": "COMPLETED"
    }
  ]
}