Routing strategy
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": []
}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": {
// 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": [
// RoutingStrategyStandardFence and RoutingStrategyToolkitFence
],
"ratings": [
// RoutingStrategyStandardRating and RoutingStrategyToolkitRating
],
"orderSplit": {
// RoutingStrategyOrderSplitConfig
},
"reroute": {
// RoutingStrategyRerouteConfig
}
}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
General Advice: with every API operation it is good practice to note down the
versionandidof 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.
Using a REST client such as Postman you should call the following endpoint:
{{baseURL}}/api/routing/strategies with method POST and as JSON-body we send the absolute minimum RoutingStrategyForCreation. Remember to send the usual Bearer-Token authorization headers.
{
"nameLocalized": {
"en_US": "Initial RoutingStrategy"
}
}Assuming you have followed the steps you should have gotten the created routing strategy as response. It should contain global routing config as well as a root node with a basic routing configuration, meaning all standard ratings and fences are present but inactive. The routing strategy itself will also be inactive until you activate it.
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 RoutingStrategyroutingStrategyVersion- the current version of theRoutingStrategyas from the last responsenode- theRoutingStrategyNodethat we just created in step 2.4condition- theRoutingStrategyConditionthat 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
}
}Using REST requires you to send the entire RoutingStrategy in the request body so you’ll need to assemble it manually.
Copy the current
RoutingStrategy. Use theGET /api/routing/strategies/:strategyIdendpoint if necessaryfind the field named
rootNodein yourRoutingStrategyadd a field named
nextConditionto yourRootNodeset the
nextConditionfield to theRoutingStrategyConditionthat we created in step 2.3add the field
nextNodeto yourRoutingStrategyConditionwithin the
RoutingStrategyConditionset thenextNodefield to theRoutingStrategyNodethat we created in step 2.4
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
Using REST:
We prepare a
POSTtoapi/routing/strategies/:strategyId/actionsWe set the parameter
strategyIdto theidof theRoutingStrategywe want to testWe set the request body to JSON and send the
OrderForCreationwe want to testThe response should contain the following:
evaluatedPathandevaluatedConfigThe
evaluatedConfigshould contain the actualRoutingStrategyNodeConfigthat would get used for routing this particular orderThe
evaluatedPathcontains information on how theevaluatedConfigis assembled. It has no other purpose than creating transparency and helping understanding the elements that got evaluated.Take a look at the
evaluatedConfigand take a closer look at theGeodistanceRatingIf you sent the first order (without tags), the rating should be inactive with a
maxPenaltyof0If you sent the second order (containing the tag), the rating should be active with a
maxPenaltyof1000Repeat previous the steps for the other order as well
If everything works out as expected you can now be sure that the
RoutingStrategywill 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.
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.
Using the endpoint {{baseURL}}/api/routing/strategies/:strategyId/actions with the method POST and following body you should have successfully activated the RoutingStrategy
{
"name": "ACTIVATE"
}Last updated