Routing strategy (context-based multi-config DOMS)

Why we need it

The current Routing Configuration has a wide range of applications but falls short in certain use cases. For one, different countries may want to use different configurations for legal or economic reasons. Maybe in certain regions, the demand calls for fast deliveries, or in other regions, there is no economic incentive for features such as order split. Another example would be to treat certain ratings, like the Geodistance rating, differently for orders with line items that need pallets and thus require freight forwarding.

Basic Concepts

We have a tree-like configuration that we call RoutingStrategy. Every RoutingStrategy has a mandatory RootNode that serves as an entry point and contains the baseline routing configuration that can be extended and altered by nested nodes. A node can be active or inactive and is followed by a condition. The condition defines a node that gets evaluated if certain criteria are met and may define a condition to be checked if the criteria are not met.

Setup

Entities

RoutingStrategy

This is the entity that encompasses all configurations. There can be multiple RoutingStrategies defined but only one may be in use (denoted by the inUse flag). It requires a localized name which can help distinguish between one strategy and another. The revision denotes which RoutingStrategy is the latest. If a new RoutingStrategy gets created it automatically gets the highest revision number.

{
  nameLocalized: { 
    en_US: "Routing strategy",
    de_DE: "Routingstrategie" 
  },
  rootNode: { 
    // RoutingStrategyNode
  }, )
  globalConfiguration: {
    // RoutingStrategyGlobalConfiguration
  }, 
  version: 1,
  revision: 3
}

RoutingStrategyGlobalConfiguration

{
  stopRoutingAttemptsAfterTime: "PT1M",
  defaultPrice: 10,
  timeTriggered: {
    shipFromStoreDeliveryReroute: { /*...*/ },
    shipFromStoreSameDayReroute: { /*...*/},
    clickAndCollectReroute: { /*...*/ }
  }
}

RoutingStrategyNode

These are the entities containing actual routing configurations and specific rules. At least one RootNode has to be defined. A node may define a condition referencing another node. It may be active or not and in addition to that it may specify a number of time frames during which the rule is applicable. At least one ActivationTimeFrame has to apply and the active field has to be true. Both criteria have to be met, meaning an inactive node will always be inactive, regardless of the activationTimeFrames field.

{
  nameLocalized: { en_US: "Routing strategy node", de_DE: "Routingstrategieknoten" },
  active: true,
  config: { /*...*/ }, //(RoutingStrategyNodeConfig),
  nextCondition: { /*...*/ }, //(optional, RoutingStrategyCondition)
  activationTimeFrames: [], //(Array of ActivationTimeFrame)
}

ActivationTimeFrame

An entity defining the valid timeframe for a node to be active (e.g. a Christmas sale). It can be either NONRECURRING or recurring YEARLY.

{
  activeFrom: "2024-12-24",
  activeUntil: "2024-12-31",
  recurrence: "YEARLY"
}

If a recurrence is set to yearly it will repeat every year after the given start date.

RoutingStrategyCondition

A condition is the fundamental part of our dynamic routing rules. It contains a rule and links to a RoutingStrategyNode that applies if the rule gets matched. Also it may define a following condition to be evaluated if the rule does not apply. The following condition for example applies to orders with German delivery addresses. Just like a node it may also define a number of ActivationTimeFrames.

{
  nameLocalized: { en_US: "Orders to Germany", de_DE: "Bestellungen nach Deutschland" },
  rule: {
    predicateConnector: "OR",
    predicates: [{
      propertyPath: "$.order.consumer.addresses[?(@.type === 'POSTAL_ADDRESS')].country",
      expectedValue: "Germany",
      entityOperator: "ANY_VALUE_EQUALS"
    }]
  },
  nextNode: { /*...*/ }, //(RoutingStrategyNode),
  nextCondition: { /*...*/ }, //(optional, could be a condition checking for another address country),
  activationTimeFrames: { /*...*/ }, //(Array of ActivationTimeFrame)
}

RoutingStrategyNodeConfig

A routing configuration defined by each RoutingStrategyNode. Every part of this can be overwritten or extended by deeper nested RoutingStrategyNodeConfigs. They contain the core part of the DOMS Routing functionality such as Fences, Ratings and an order-split/reroute configuration. However unlike before, toolkit fences and ratings now lie in the same place as their standard counterparts.

{
  fences: [ /*...*/ ], // Array of RoutingStrategyStandardFence and RoutingStrategyToolkitFence
  ratings: [ /*...*/ ], // Array of RoutingStrategyStandardRating and RoutingStrategyToolkitRating,
  orderSplit: { /*...*/ }, // RoutingStrategyOrderSplitConfig,
  reroute: { /*...*/ }, // RoutingStrategyRerouteConfig,
  fallbackFacility
}

RoutingStrategyStandardFence

A fence using a predefined implementation. It’s nearly identical to the previously existing Fence model but contains a field called type denoting that it’s a StandardFence.

{
  type: "ToolkitFence",
  referenceId: "unique-id",
  rule: { /*...*/ }, //(ToolkitRule)
  comparisonRule: { /*...*/ }, //(ToolkitComparisonRule, mutually exclusive with ToolkitRule)
}

RoutingStrategyToolkitRating

A rating created using the Toolkit functionality. In addition to the existing ToolkitRating it is required now to add a referenceId to identify a toolkit rating because it may appear on multiple layers of the RoutingStrategy and changes to it may affect more than one entity. Similar to its standard counterpart a required field type is also added denoting that it’s a ToolkitRating.

{
  type: "ToolkitRating",
  referenceId: "unique-id",
  rule: { /*...*/ }, //(ToolkitRule)
  comparisonRule: { /*...*/ }, //(ToolkitComparisonRule, mutually exclusive with ToolkitRule)
}

Example: RoutingStrategy with special configs for pallet

Click here if you want to see the final config
{
  "id": "some-unique-id-set-by-the-backend",
  "name": "Initial RoutingStrategy",
  "nameLocalized": { "en_US": "Initial RoutingStrategy" },
  "version": 3,
  "revision": 1,
  "rootNode": {
    "id": "some-unique-id-set-by-the-backend",
    "name": "Root Node",
    "nameLocalized": { "en_US": "Root Node" },
    "config": {
      "fences": [],
      "ratings": []
    },
    "nextCondition": {
      "id": "some-unique-id-set-by-the-backend",
      "nameLocalized": { "en_US": "Order requires pallets" },
      "active": true,
      "rule": {
        "predicates": [
            {
                "propertyPath": "$.order.orderLineItems[?(@.tags.find(tag => tag.id === 'load-unit' && value === 'pallet')]",
                "transformation": "COUNT",
                "entityOperator": "GREATER_EQUALS",
                "expectedValue": 1
            }
        ],
        "predicateConnector": "AND"
      },
      "nextNode": {
        "id": "some-unique-id-set-by-the-backend",
        "active": true,
        "nameLocalized": { "en_US": "Pallet routing configuration" },
        "config": {
          "ratings": [
            {
              "implementation": "GEO-DISTANCE",
              "type": "StandardRating",
              "maxPenalty": 1000,
              "active": true
            }
          ]
        }
      }
    }
  }
}

General Advice: with every API operation it is good practice to note down the version and id of the entity you are manipulating/accessing as you are likely to need it in future steps. It may also be helpful to save the entire entity, especially for PUT operations.

Step 1 - Set up your RoutingStrategy using the respective endpoint

Before you get started, request a new auth token and add the headers as described in the quickstart documentation. The baseURL variable will be https://{YOUR_TENANT_NAME}.api.fulfillmenttools.com when using the REST API. When using GraphQL, replace the api subdomain with graphql.

First of all you will create a RoutingStrategyForCreation. The absolute minimum required here is to give your strategy a localized name to distinguish it from other strategies you might create down the road. We have two options here - using the GraphQL resolver or the REST controller. We will use the name “Initial RoutingStrategy” for our English locale and leave every other field empty for now.

The following shows the mutation we want to use to create the initial routing strategy. You can copy & paste it into any GraphQL client (such as Altair) and it should work out of the box assuming you have set the Authorization headers using a valid bearer token.

mutation createRoutingStrategy {
    createRoutingStrategy(input: { nameLocalized: { en_US: "Initial RoutingStrategy" }}) {
        routingStrategy {
            id
            version
            nameLocalized
            rootNodeRef
            nodes {
                id
                active
                nameLocalized
                name
                config {
                    fences {
                        __typename
                        ... on RoutingStrategyStandardFence {
                            implementation
                            name
                            active
                        }
                    }
                    ratings  {
                        __typename
                        ... on RoutingStrategyStandardRating {
                            implementation
                            name
                            active
                        }
                    }
                }
            }
        }
    }
}

If everything has worked out you should see that we have a basic routing strategy with all standard fences with active set to false and no additional configurations. However it won’t be active yet so let’s do that now.

Step 2 - Adding a Condition and Node to the existing RoutingStrategy

We want to create a new condition that applies to orders having line items with pallet articles. Let’s assume the OrderLineItem will have a tag that looks as follows { id: ‘load-unit’, value: ‘pallet’ }. Using that we will build a toolkit rule part that evaluates to true if a line item with this tag.

Step 2.1 - Creating the JSONPath

The entities that will get evaluated are of the following schema:

{
    order: OrderForCreation,
    routingPlan?: RoutingPlan
}

Therefore to get a JSONPath pointing to the tag we should use the following string:

$.order.orderLineItems[?(@.tags.find(tag => tag.id === "load-unit" && value === "pallet")]

Step 2.2 - Creating the ToolkitPredicate with the JSONPath

This JSONPath will get all OrderLineItems that have the tag that we defined with the appropriate value. Since the result we got is an array we can use the COUNT transformer to count if we have at least one OrderLineItem satisfying the properties. Since we only want to know if there is at least one (read: count greater equals 1) such order line item we can define the entire predicate as follows.

{
    "propertyPath": "$.order.orderLineItems[?(@.tags.find(tag => tag.id === 'load-unit' && value === 'pallet')]",
    "transformation": "COUNT",
    "entityOperator": "GREATER_EQUALS",
    "expectedValue": 1
}

Step 2.3 - Creating a RoutingStrategyCondition with the ToolkitPredicate

The ToolkitRulePart that we assemble will contain an array containing only this predicate and a ToolkitPredicateConnector which becomes important as more predicates get added to the array. We’ll set it to ”AND” for now. Then we create a condition containing the rule that looks as follows:

{
    "nameLocalized": {
        "en_US": "Order requires pallets"
    },
    "active": true,
    "rule": {
        "predicates": [
            {
                "propertyPath": "$.order.orderLineItems[?(@.tags.find(tag => tag.id === 'load-unit' && value === 'pallet')]",
                "transformation": "COUNT",
                "entityOperator": "GREATER_EQUALS",
                "expectedValue": 1
            }
        ],
        "predicateConnector": "AND"
    },
    "nextNode": {}
}

The node we want to create should have a config that applies only to orders fulfilling that condition so we should add a config that extends the existing default routing config.

Step 2.4 - Creating a RoutingStrategyNode

Now that we’ve created a ToolkitRulePart we can create a RoutingStrategyCondition that uses this rule. Let’s name it “Pallet routing configuration” since we will add the appropriate routing configuration to it. The RoutingStrategyNode could look as follows:

{
    "active": true,
    "nameLocalized": {
        "en_US": "Pallet routing configuration"
    },
    "config": {
        "ratings": [
            {
                "implementation": "GEO-DISTANCE",
                "type": "StandardRating",
                "maxPenalty": 1000,
                "active": true
            }
        ]
    }
}

The configuration of the node contains only one rating. What this means in practice is that every configuration will be inherited from the root config (fences, ratings, reroute configuration) and only this particular fence will be overwritten.

Step 2.5 - Updating the RoutingStrategy with the new node and condition

Depending on whether you use GraphQL or not there are two ways we can update the RoutingStrategy

The following mutation adds our RoutingStrategyNode and RoutingStrategyCondition to the root node. However you also need to fill in the following variables:

  • strategyRef - the id of our created RoutingStrategy

  • routingStrategyVersion - the current version of the RoutingStrategy as from the last response

  • node - the RoutingStrategyNode that we just created in step 2.4

  • condition - the RoutingStrategyCondition that we created in step 2.3

mutation appendRoutingStrategyConditionAndNodeToNode ($node: RoutingStrategyNodeCreationInput!, $condition: RoutingStrategyConditionCreationInput!, $strategyRef: String!, $rootNodeRef: String!, $routingStrategyVersion: Int!) {
    appendRoutingStrategyConditionAndNodeToNode(
        input: {
            strategyRef: $strategyRef
            nodeRef: $rootNodeRef
            condition: $condition
            node: $node
            routingStrategyVersion: $routingStrategyVersion
        }
    ) {
        routingStrategy {
            id
            created
            lastModified
            version
            revision
            inUse
            nameLocalized
            name
            rootNodeRef
            nodes {
                active
                nameLocalized
                config {
                    ratings {
                        __typename
                        ... on RoutingStrategyStandardRating {
                            active
                            maxPenalty
                            implementation
                        }
                    }
                }
            }
            conditions {
                active
                nameLocalized
                rule {
                    predicateConnector
                    predicates {
                        propertyPath
                        entityOperator
                        expectedValue
                        transformation
                    }
                }
            }
        }
        routingStrategyVersion
    }
}

Step 3 - Creating an order to test the configurations

The entire purpose of MultiDOMS is to have a dynamic routing configuration that behaves differently in depending on context. So naturally we should test our configuration with two different orders.

Step 4.1 - Creating a "regular" order that should use the default configuration

First, we should test that a regular order (i.e. not satisfying the condition that we defined) will use the default configuration, meaning standard fences / ratings all set to inactive. The order could look something like this:

{
    "consumer": {
        "addresses": [
            {
                "city": "Cologne",
                "country": "Germany",
                "street": "Schanzenstrasse",
                "postalCode": "51063"
            }
        ]
    },
    "orderDate": "2025-01-30T08:15.000Z",
    "orderLineItems": [
        {
            "article": {
                "title": "Gaffel Wiess"
            },
            "quantity": 4711
        }
    ]
}

Step 4.2 - Creating a "special" order that should use the special configuration

Then we define an order that does meet the defined condition. It could look something like this:

{
    "consumer": {
        "addresses": [
            {
                "city": "Cologne",
                "country": "Germany",
                "street": "Schanzenstrasse",
                "postalCode": "51063"
            }
        ]
    },
    "orderDate": "2025-01-30T08:15.000Z",
    "orderLineItems": [
        {
            "article": {
                "title": "Gaffel Wiess vom Fass"
            },
            "quantity": 100,
            "tags": [
                {
                    "id": "load-unit",
                    "value": "pallet"
                }
            ]
        }
    ]
}

To keep things simple we kept the order mostly the same except for the discriminator, which is the tags array on OrderLineItem level. In the next step we’ll evaluate the RoutingStrategy with these two orders.

Step 4 - Testing the RoutingStrategy evaluation

At the moment we do not offer a GraphQL Query for this yet

Using REST:

  1. We prepare a POST to api/routing/strategies/:strategyId/actions

  2. We set the parameter strategyId to the id of the RoutingStrategy we want to test

  3. We set the request body to JSON and send the OrderForCreation we want to test

  4. The response should contain the following: evaluatedPath and evaluatedConfig

  5. The evaluatedConfig should contain the actual RoutingStrategyNodeConfig that would get used for routing this particular order

  6. The evaluatedPath contains information on how the evaluatedConfig is assembled. It has no other purpose than creating transparency and helping understanding the elements that got evaluated.

  7. Take a look at the evaluatedConfig and take a closer look at the GeodistanceRating

  8. If you sent the first order (without tags), the rating should be inactive with a maxPenalty of 0

  9. If you sent the second order (containing the tag), the rating should be active with a maxPenalty of 1000

  10. Repeat previous the steps for the other order as well

  11. If everything works out as expected you can now be sure that the RoutingStrategy will use the correct configuration for the given examples. It is advised to do this for every single change that you do. If you’re uncertain, do not activate the RoutingStrategy until you have done this step.

Step 5 - Activate the RoutingStrategy to use it

Assuming the MultiDOMS feature is enabled on the given tenant, activating it will make the RoutingStrategy become the single source of truth for routing decisions so please skip this step until you are certain you want to use it.

The mutation we will use is activateRoutingStrategy and requires an activateRoutingStrategyInput which contains the routingStrategyId and version you have copied from the last step.

Forgot to jot down the id and version? Use this query!
query routingStrategies {
    routingStrategies(first: 1) {
        pageInfo {
            hasNextPage
        }
        edges {
            node {
                id
            }
        }
    }
}
mutation activateRoutingStrategy {
    activateRoutingStrategy(
        activateRoutingStrategyInput: { routingStrategyId: "string", version: 1 }
    ) {
        routingStrategy {
            inUse
        }
        routingStrategyVersion
    }
}

The response should show that the RoutingStrategy has now its inUse property set to true.

Last updated