ReST vs CQRS: The Trigger Pattern
Matt Hawkins @mattchawkins
Introduction
I've found that most online examples/blogs of Command Query Responsibility Segregation (CQRS) with Event Sourcing (ES) and Domain Driven Design (DDD) are fairly trivial. After trudging the road to create one of these behemoths on a production scale, in a complex financial domain, I’ve discovered that CQRS and ReST can be quite orthogonal.
The problem arises when you consider the limited number of HTTP methods that you have to execute non-query requests on the ReSTful API. In this blog post we’re going to focus on two verbs: POST and PUT. How do you perform multiple complex state changes with one or two verbs while still being ReSTful and not providing explicit intent to the API? I’ve found a few compelling solutions to this problem, with the winner being the trigger pattern.
Part 1: Domain Model
Let’s consider the domain of a Car
. We’ll assume this domain requires a granular log of
all state changes. Obviously we wouldn’t be changing CarID
, but it’s likely the Color
,
Mileage
, Condition
, and Owner
may change during a car’s lifecycle.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: |
|
Above is the Domain Model of a
Car
. All of these associated domain types represent an Aggregate in Domain Driven Design.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
|
Events model all possible state changes in a granular delta representation; the event only contains what has changed. There are a few different approaches to modeling events (granular delta's, snapshots, and before-and-after’s); depending on the requirement, you can choose accordingly.
Part 2: The ReSTful API
Providing an API to your domain model is how you affect change on it. Below we have two API
endpoints, a POST and a PUT. For this example, POST will be used to create an instance of
Car
and PUT will be used to update an instance of Car
. CQRS API's are typically
light weight and have two job's, to fire-and-forget commands and to provide a data
via query (GET).
1: 2: 3: 4: 5: 6: 7: 8: |
|
The POST endpoint parses the JSON body of the request:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:{ "year": 2009, "make": "BMW", "model": "M3", "color": "black", "mileage": 14320, "vin": "2G4GM5ER4E9225618", "condition": "good", "owner": "ad4181f8-c33f-4e81-9d9e-0896188d73f0" }
Then pipes the deserialized
Car
representation into an instance of aCreate
command and places it on the command queue.
1: 2: 3: 4: 5: 6: 7: |
|
The PUT endpoint is very similar to the POST endpoint except it includes the identifier of the
Car
in the URI as shown above. We simply use the F#with
keyword to effectivly update theCar
representation with theCarID
, pipe it into an instance ofUpdate
command, and place the command on the command queue.
Part 3: The Command & Trigger Patterns
As shown above, the API publishes commands onto the command queue, which is then consumed by a command handler. Let's take a closer look at the commands:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: |
|
Notice the only responsiblity of the command is to encapsulate the
Car
's API representation,CarRepresentation
.
Now let's examine how the command is handled. The commandHandler
is responsible for
consuming the command, validating it, and working with the Aggregate Root. The root
may return an event which is then persisted and broadcasted.
1: 2: 3: 4: 5: 6: |
|
The TriggerBuilder.map
function takes an API command, data-type validates it, and maps it
into domain trigger(s). Data-type validation is simply confirming that Guid
's are not empty and strings
map
to their proper enum
's, etc.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
|
CarTriggers
Trigger domain type provides explicit actions you can take against the Aggregate.CarCommands
from the API have implicit intent, as you're just PUT'ing the future state of theCar
to the API, whereasCarTriggers
have explicit intent, and provide a precise modeling mechanism for complex state changes.
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
The
TriggerBuilder
matches on theCarCommands
type and provides theCarRepresentation
from the command to the mapping function (mapCreate
,mapUpdate
, ormapDelete
).
The responsibility of the trigger mapping functions (mapCreate
, mapUpdate
, or mapDelete
), are to
1) data-type validate the representation 2) determine which domain triggers to return. The data-type
validation is quite trivial:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
Simple type validation and mapping of primitives to value types. ("Value Types" in the context of Domain Driven Design)
1: 2: 3: 4: |
|
mapCreate
validates & maps the API representation CarRepresentation
, then makes a Create
trigger, finally
mapping into a Tuple
with the zero state of the Aggregate Root (see Part 4). The trigger builder will
return this ('State * 'Triggers list)
tuple back to the Command Handler, where it is then fired against
the Aggregate Root; thus producing domain events.
Finally the part you've been waiting for. How does the system magically figure out what state been changed from the PUT API representation? Truth is, it's not that magical.
1: 2: 3: 4: |
|
mapUpdate
data-type validates theCarRepresentation
the same way themapCreate
function does. Thecompare
function is where the analysis happens.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: |
|
Each
eval
function evaluates the current and proposed state, returning aTrigger option
.
1: 2: 3: 4: |
|
evalColor
evaluates current and proposed color, returning aChangeColor option
(Trigger).
To summarize the Command Handler, it consists of the following components:
1. Trigger Builder
-Maps "implicit intent" API Commands to "explicit intent" Domain Triggers.
2. Execution
-Involves rehydration of the Aggregate from the event store and firing of the Triggers against the Aggregate Root, which result in Domain Events.
3. Persistence & Broadcasting
-Saving the Domain Events to durable storage (Event Store) and broadcasting said events to interested parties.
Part 4: CQRS Aggregate Root
The Domain Driven Design Aggregate Root is the "chosen" entity that is responsible for consistency
within the aggregates type's and acts as a liason to the outside world. In this example the Aggregate
Root was a Car
. Consider the abstract notion of the DDD Aggregate Root with the following concrete
type definition of a CQRS Aggregate Root:
1: 2: 3: 4: |
|
AggregateRoot
type implemented for theCar
aggregate root:
1: 2: 3: 4: |
|
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: |
|
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: |
|
Notice the
AggregateRoot
type has 3 functions.Zero
provides a starting state for the Aggregate Root, frequently used during rehydration/hydration of Domain Events.Apply
provides model rehydration functionality.Fire
is a function that takes the current state of the Aggregate and a Domain Trigger, and returns a Domain Event.
Closing Thoughts
There are many ways to expose domain actions through an API. The Trigger Pattern approach is one that strives to allow you to maintain somewhat of a ReSTful API. Another approach I've encountered (but haven't tested), is moving the "trigger" logic into the client, effectively placing a command-type in the request headers. I feel this approach is may be moving you torwards RPC rather than ReST, but it is another option. A similar alternative is just using the PATCH verb, where the client supplies the delta change to the API, effectively eliminating the need for Triggers in the most trivial of cases.
Written by:
Matt Hawkins |
December 18th 2015 |
Twitter: @mattchawkins |
| Create of CarRepresentation
| Update of CarRepresentation
| Delete of carID: Guid
Full name: Post.CarCommands
{CarID: Guid;
Year: int;
Make: string;
Model: string;
Color: string;
Mileage: int;
VIN: string;
Condition: string;
Owner: Guid;}
Full name: Post.CarRepresentation
type Guid =
struct
new : b:byte[] -> Guid + 4 overloads
member CompareTo : value:obj -> int + 1 overload
member Equals : o:obj -> bool + 1 overload
member GetHashCode : unit -> int
member ToByteArray : unit -> byte[]
member ToString : unit -> string + 2 overloads
static val Empty : Guid
static member NewGuid : unit -> Guid
static member Parse : input:string -> Guid
static member ParseExact : input:string * format:string -> Guid
...
end
Full name: System.Guid
--------------------
Guid()
Guid(b: byte []) : unit
Guid(g: string) : unit
Guid(a: int, b: int16, c: int16, d: byte []) : unit
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : unit
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : unit
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = String
Full name: Microsoft.FSharp.Core.string
--------------------
type string<'Measure> = string
Full name: ExtCore.string<_>
{Body: CarRepresentation;}
Full name: Post.HTTPRequest
Full name: Post.sendToQueue
Full name: Post.extractID
Full name: Post.OK
Full name: Post.Accepted
Returns a 202 status code
module Choice
from ExtCore
--------------------
type Choice<'T1,'T2> =
| Choice1Of2 of 'T1
| Choice2Of2 of 'T2
Full name: Microsoft.FSharp.Core.Choice<_,_>
--------------------
type Choice<'T1,'T2,'T3> =
| Choice1Of3 of 'T1
| Choice2Of3 of 'T2
| Choice3Of3 of 'T3
Full name: Microsoft.FSharp.Core.Choice<_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4> =
| Choice1Of4 of 'T1
| Choice2Of4 of 'T2
| Choice3Of4 of 'T3
| Choice4Of4 of 'T4
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4,'T5> =
| Choice1Of5 of 'T1
| Choice2Of5 of 'T2
| Choice3Of5 of 'T3
| Choice4Of5 of 'T4
| Choice5Of5 of 'T5
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4,'T5,'T6> =
| Choice1Of6 of 'T1
| Choice2Of6 of 'T2
| Choice3Of6 of 'T3
| Choice4Of6 of 'T4
| Choice5Of6 of 'T5
| Choice6Of6 of 'T6
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4,'T5,'T6,'T7> =
| Choice1Of7 of 'T1
| Choice2Of7 of 'T2
| Choice3Of7 of 'T3
| Choice4Of7 of 'T4
| Choice5Of7 of 'T5
| Choice6Of7 of 'T6
| Choice7Of7 of 'T7
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_,_,_>
Full name: Post.Choice.packSequence
module Seq
from ExtCore.Collections
--------------------
module Seq
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.map
member Clone : unit -> obj
member CopyTo : array:Array * index:int -> unit + 1 overload
member GetEnumerator : unit -> IEnumerator
member GetLength : dimension:int -> int
member GetLongLength : dimension:int -> int64
member GetLowerBound : dimension:int -> int
member GetUpperBound : dimension:int -> int
member GetValue : [<ParamArray>] indices:int[] -> obj + 7 overloads
member Initialize : unit -> unit
member IsFixedSize : bool
...
Full name: System.Array
Full name: Microsoft.FSharp.Collections.Array.ofSeq
Full name: Microsoft.FSharp.Collections.Array.partition
val obj : bool * Choice<'a,'b>
--------------------
type obj = Object
Full name: Microsoft.FSharp.Core.obj
Full name: Microsoft.FSharp.Core.Operators.fst
Full name: ExtCore.Choice.result
Full name: Microsoft.FSharp.Collections.Array.map
Full name: Microsoft.FSharp.Core.Operators.snd
Full name: ExtCore.Choice.get
Full name: Microsoft.FSharp.Collections.Seq.ofArray
Full name: ExtCore.Choice.error
Full name: ExtCore.Choice.getError
Full name: Post.EventStore.rehydrate
module Choice
from Post
--------------------
module Choice
from ExtCore
--------------------
type Choice<'T1,'T2> =
| Choice1Of2 of 'T1
| Choice2Of2 of 'T2
Full name: Microsoft.FSharp.Core.Choice<_,_>
--------------------
type Choice<'T1,'T2,'T3> =
| Choice1Of3 of 'T1
| Choice2Of3 of 'T2
| Choice3Of3 of 'T3
Full name: Microsoft.FSharp.Core.Choice<_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4> =
| Choice1Of4 of 'T1
| Choice2Of4 of 'T2
| Choice3Of4 of 'T3
| Choice4Of4 of 'T4
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4,'T5> =
| Choice1Of5 of 'T1
| Choice2Of5 of 'T2
| Choice3Of5 of 'T3
| Choice4Of5 of 'T4
| Choice5Of5 of 'T5
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4,'T5,'T6> =
| Choice1Of6 of 'T1
| Choice2Of6 of 'T2
| Choice3Of6 of 'T3
| Choice4Of6 of 'T4
| Choice5Of6 of 'T5
| Choice6Of6 of 'T6
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_,_>
--------------------
type Choice<'T1,'T2,'T3,'T4,'T5,'T6,'T7> =
| Choice1Of7 of 'T1
| Choice2Of7 of 'T2
| Choice3Of7 of 'T3
| Choice4Of7 of 'T4
| Choice5Of7 of 'T5
| Choice6Of7 of 'T6
| Choice7Of7 of 'T7
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_,_,_>
from Microsoft.FSharp.Core.Operators
Full name: Microsoft.FSharp.Core.Operators.Unchecked.defaultof
Full name: Post.EventStore.saveEvents
Full name: Post.some
Full name: Post.broadcast
Broadcasts events to interested parties
Full name: Post.postEndpoint
Full name: Post.putEndpoint
type CustomEqualityAttribute =
inherit Attribute
new : unit -> CustomEqualityAttribute
Full name: Microsoft.FSharp.Core.CustomEqualityAttribute
--------------------
new : unit -> CustomEqualityAttribute
type NoComparisonAttribute =
inherit Attribute
new : unit -> NoComparisonAttribute
Full name: Microsoft.FSharp.Core.NoComparisonAttribute
--------------------
new : unit -> NoComparisonAttribute
{CarID: Guid;
Year: int;
CarType: CarType;
Color: Color;
Mileage: int;
VIN: string;
Condition: Condition;
Owner: Guid;}
override Equals : that:obj -> bool
override GetHashCode : unit -> int
Full name: Post.Car
Car.CarType: CarType
--------------------
type CarType =
| Ford of Ford
| BMW of BMW
Full name: Post.CarType
Car.Color: Color
--------------------
type Color =
| Red
| White
| Blue
Full name: Post.Color
Car.Condition: Condition
--------------------
type Condition =
| Excellent
| Good
| Average
| Poor
Full name: Post.Condition
Full name: Post.Car.Equals
Full name: Microsoft.FSharp.Core.obj
Full name: Post.Car.GetHashCode
Full name: Microsoft.FSharp.Core.Operators.hash
| Ford of Ford
| BMW of BMW
Full name: Post.CarType
union case CarType.Ford: Ford -> CarType
--------------------
type Ford =
| Escape
| F150
Full name: Post.Ford
union case CarType.BMW: BMW -> CarType
--------------------
type BMW =
| M3
| M5
Full name: Post.BMW
| Escape
| F150
Full name: Post.Ford
| M3
| M5
Full name: Post.BMW
| Red
| White
| Blue
Full name: Post.Color
| Excellent
| Good
| Average
| Poor
Full name: Post.Condition
| Created of carID: Guid * year: int * CarType * Color * mileage: int * vin: string * owner: Guid * condition: Condition
| ColorChanged of Color
| MileageChanged of mileage: int
| ConditionChanged of Condition
| OwnerChanged of owner: Guid
Full name: Post.CarEvents
| Create of carID: Guid * year: int * CarType * Color * mileage: int * vin: string * owner: Guid * condition: Condition
| ChangeColor of Color
| UpdateMileage of mileage: int
| UpdateCondition of Condition
| ChangeOwner of owner: Guid
Full name: Post.CarTriggers
Full name: Post.DomainError
type String =
new : value:char -> string + 7 overloads
member Chars : int -> char
member Clone : unit -> obj
member CompareTo : value:obj -> int + 1 overload
member Contains : value:string -> bool
member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
member EndsWith : value:string -> bool + 2 overloads
member Equals : obj:obj -> bool + 2 overloads
member GetEnumerator : unit -> CharEnumerator
member GetHashCode : unit -> int
...
Full name: System.String
--------------------
String(value: nativeptr<char>) : unit
String(value: nativeptr<sbyte>) : unit
String(value: char []) : unit
String(c: char, count: int) : unit
String(value: nativeptr<char>, startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int) : unit
String(value: char [], startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : unit
{Zero: unit -> 'state;
Apply: 'state -> 'event -> 'state;
Fire: 'state -> 'trigger -> Choice<'event,DomainError>;}
Full name: Post.AggregateRoot<_,_,_>
Full name: ExtCore.Control.WorkflowBuilders.state
Full name: Microsoft.FSharp.Core.unit
Full name: Post.validateID
Full name: Post.validateYear
Full name: Post.isNonNegative
Full name: Post.isGreaterThan
Full name: Post.fireFn
Full name: ExtCore.Control.WorkflowBuilders.choice
Full name: Post.applyFn
Full name: Post.root
Full name: Post.mapEnum
Full name: Microsoft.FSharp.Core.Operators.enum
member CompareTo : target:obj -> int
member Equals : obj:obj -> bool
member GetHashCode : unit -> int
member GetTypeCode : unit -> TypeCode
member HasFlag : flag:Enum -> bool
member ToString : unit -> string + 3 overloads
static member Format : enumType:Type * value:obj * format:string -> string
static member GetName : enumType:Type * value:obj -> string
static member GetNames : enumType:Type -> string[]
static member GetUnderlyingType : enumType:Type -> Type
...
Full name: System.Enum
Enum.Parse(enumType: Type, value: string, ignoreCase: bool) : obj
Full name: Microsoft.FSharp.Core.Operators.typedefof
Full name: Post.isValidString
Full name: Post.mapCarType
String.Trim([<ParamArray>] trimChars: char []) : string
Full name: ExtCore.Choice.bind
Full name: Post.validateRepresentation
Full name: Post.mapCreate
Full name: ExtCore.Choice.map
module List
from ExtCore.Collections
--------------------
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IEnumerable
interface IEnumerable<'T>
member GetSlice : startIndex:int option * endIndex:int option -> 'T list
member Head : 'T
member IsEmpty : bool
member Item : index:int -> 'T with get
member Length : int
member Tail : 'T list
static member Cons : head:'T * tail:'T list -> 'T list
static member Empty : 'T list
Full name: Microsoft.FSharp.Collections.List<_>
val singleton : value:'T -> 'T list
Full name: ExtCore.Collections.List.singleton
--------------------
val singleton : value:'T -> 'T list
Full name: Microsoft.FSharp.Collections.List.singleton
Full name: Post.evalColor
Full name: Post.evalMileage
Full name: Post.evalOwner
Full name: Post.evalCondition
Full name: Post.compare
Full name: Microsoft.FSharp.Collections.List.choose
Full name: Post.mapUpdate
from Post
Full name: Post.mapDelete
Full name: Post.TriggerBuilder.map
Full name: Post.execute
Full name: Microsoft.FSharp.Collections.Seq.fold
Full name: Microsoft.FSharp.Collections.List.append
Full name: Microsoft.FSharp.Collections.Seq.last
Full name: ExtCore.Choice.mapError
Full name: ExtCore.String.concatArray
Full name: Post.commandHandler
from Post