Command pattern
Problem
Mapping CRUD operations to semantics of HTTP POST
, PUT
, DELETE
is easy. However that is not the case for more complex operations that do more
than simply send the new state of a single resource. An example of such operations is the renewal of offers on Allegro - the operation requires some input data
and modifies lots of fields in the given offer causing business changes in many integrated services (settlement, fraud monitoring, etc.).
How to model this type of a non-trivial operation in REST? It can be a change caused by updating the offers status as presented below:
curl -X PUT https://api.allegro.pl/offers/6546456 -H "Content-Type: application/vnd.allegro.public.v1+json" -d
{
"status" : "RENEWED",
// all other offer fields ...
}
or this way:
curl -X PATCH https://api.allegro.pl/offers/6546456 -H "Content-Type: application/vnd.allegro.public.v1+json" -d
[
{
"op" : "replace",
"path" : "/status",
"value" : "RENEWED"
}
]
However, it looks as programming database systems from the 90's where an update of one field would unleash dozens of triggers with hidden business logic, to the surprise (and dismay) of the developer.
Beside this risky programming style, you have to ask yourself the following questions:
- what if the given operation requires input data that is not present in the resources model?
- what if the operation is so complex that you must execute it asynchronously?
- what should be returned in response to PUT or PATCH then? how to trace the status of that asynchronous operation?
Solution
All the above problems can be solved by using the command pattern that gives you:
- a verbose declaration of more complex operations in the system,
- a standard mechanism to trace the status of asynchronous operations,
- an idempotent way to execute this operation.
To implement this pattern add a sub-resource with commands to your business resource, for example: /offers/{offerId}/{command-type}-commands
.
In this example if you want to execute a complex operation on an offer add the operation input data to the appropriate commands collection.
But do not use POST
to do it as POST
is not idempotent in REST – use the PUT
method and an UUID generated by the client.
Adding the command
curl -X PUT https://api.allegro.pl/offers/6546456/renew-commands/23453425-34253245-3453454-345345 -H "Content-Type: application/vnd.allegro.public.v1+json" -d
{
"fromDate" : "2015-08-30T17:00:00.000Z",
"duration" : "P7D",
// other input data for this command
}
Although many developers are used to apply PUT method only for updates, you must be aware that the semantics of this method is much wider according to the HTTP RFC. See RFC 7231 for more details.
Sample response:
201 Created
{
"status" : "RUNNING",
"fromDate" : "2015-08-30T17:00:00.000Z",
"duration" : "P7D",
// other output data given to this command
}
After adding this command, the service will execute it asynchronously.
It should return the HTTP 201 Created
status code even if you send this request many times.
however the business logic will be executed only once.
Checking execution status (optionally)
curl -X GET https://api.allegro.pl/offers/6546456/renew-commands/23453425-34253245-3453454-345345 -H "Accept: application/vnd.allegro.public.v1+json"
Response:
200 Ok
{
"status" : "SUCCESSFUL",
"fromDate" : "2015-08-30T17:00:00.000Z",
"duration" : "P7D",
// other output data given to this command
}
or if the execution of the command failed:
200 Ok
{
"status" : "FAILED",
"fromDate" : "2015-08-30T17:00:00.000Z",
"duration" : "P7D",
// other output data given to this command
"errors" : [
// errors description
]
}
It is also recommended to provide information about command status changes in an event bus. Developers will decide which mechanism they prefer.
Antypatterns replaced by this pattern
- POST-based command pattern that submits commands directly to a resource such as e.g.
/offers/546534534
- according to the RFC, the
POST
method is for not idempotent operations - no guarantee on the API level that the business logic will be executed only once
- according to the RFC, the
- sending command UUID's in a header
- this pattern is incompatible with REST and the HTTP RFC which states that non-standard headers are deprecated
- does not solve all the problems that the PUT and UUID in URI pattern solves