Saga Persistence and Event-Driven Architectures
Monday, April 20th, 2009.
When working with clients, I run into more than a couple of people that have difficulty with event-driven architecture (EDA). Even more people have difficulty understanding what sagas really are, let alone why they need to use them. I’d go so far to say that many people don’t realize the importance of how sagas are persisted in making it all work (including the Workflow Foundation team).
The common e-commerce example
We accept orders, bill the customer, and then ship them the product.
Fairly straight-forward.
Since each part of that process can be quite complex, let’s have each step be handled by a service:
Sales, Billing, and Shipping. Each of these services will publish an event when it’s done its part. Sales will publish OrderAccepted containing all the order information – order Id, customer Id, products, quantities, etc. Billing will publish CustomerBilledForOrder containing the customer Id, order Id, etc. And Shipping will publish OrderShippedToCustomer with its data.
So far, so good. EDA and SOA seem to be providing us some value.
Where’s the saga?
Well, let’s consider the behavior of the Shipping service. It shouldn’t ship the order to the customer until it has received the CustomerBilledForOrder event as well as the OrderAccepted event. In other words, Shipping needs to hold on to the state that came in the first event until the second event comes in. And this is exactly what sagas are for.
Let’s take a look at the saga code that implements this. In order to simplify the sample a bit, I’ll be omitting the product quantities.
1: public class ShippingSaga : Saga<ShippingSagaData>,
2: ISagaStartedBy<OrderAccepted>,
3: ISagaStartedBy<CustomerBilledForOrder>
4: {
5: public void Handle(OrderAccepted message)
6: {
7: this.Data.ProductIdsInOrder = message.ProductIdsInOrder;
8: }
9:
10: public void Handle(CustomerBilledForOrder message)
11: {
12: this.Bus.Send<ShipOrderToCustomer>(
13: (m =>
14: {
15: m.CustomerId = message.CustomerId;
16: m.OrderId = message.OrderId;
17: m.ProductIdsInOrder = this.Data.ProductIdsInOrder;
18: }
19: ));
20:
21: this.MarkAsComplete();
22: }
23:
24: public override void Timeout(object state)
25: {
26:
27: }
28: }
First of all, this looks fairly simple and straightforward, which is good.
It’s also wrong, which is not so good.
One problem we have here is that events may arrive out of order – first CustomerBilledForOrder, and only then OrderAccepted. What would happen in the above saga in that case? Well, we wouldn’t end up shipping the products to the customer, and customers tend not to like that (for some reason).
There’s also another problem here. See if you can spot it as I go through the explanation of ISagaStartedBy<T>.
Saga start up and correlation
The “ISagaStartedBy<T>” that is implemented for both messages indicates to the infrastructure (NServiceBus) that when a message of that type arrives, if an existing saga instance cannot be found, that a new instance should be started up. Makes sense, doesn’t it? For a given order, when the OrderAccepted event arrives first, Shipping doesn’t currently have any sagas handling it, so it starts up a new one. After that, when the CustomerBilledForOrder event arrives for that same order, the event should be handled by the saga instance that handled the first event – not by a new one.
I’ll repeat the important part: “the event should be handled by the saga instance that handled the first event”.
Since the only information we stored in the saga was the list of products, how would we be able to look up that saga instance when the next event came in containing an order Id, but no saga Id?
OK, so we need to store the order Id from the first event so that when the second event comes along we’ll be able to find the saga based on that order Id. Not too complicated, but something to keep in mind.
Let’s look at the updated code:
1: public class ShippingSaga : Saga<ShippingSagaData>,
2: ISagaStartedBy<OrderAccepted>,
3: ISagaStartedBy<CustomerBilledForOrder>
4: {
5: public void Handle(CustomerBilledForOrder message)
6: {
7: this.Data.CustomerHasBeenBilled = true;
8:
9: this.Data.CustomerId = message.CustomerId;
10: this.Data.OrderId = message.OrderId;
11:
12: this.CompleteIfPossible();
13: }
14:
15: public void Handle(OrderAccepted message)
16: {
17: this.Data.ProductIdsInOrder = message.ProductIdsInOrder;
18:
19: this.Data.CustomerId = message.CustomerId;
20: this.Data.OrderId = message.OrderId;
21:
22: this.CompleteIfPossible();
23: }
24:
25: private void CompleteIfPossible()
26: {
27: if (this.Data.ProductIdsInOrder != null && this.Data.CustomerHasBeenBilled)
28: {
29: this.Bus.Send<ShipOrderToCustomer>(
30: (m =>
31: {
32: m.CustomerId = this.Data.CustomerId;
33: m.OrderId = this.Data.OrderId;
34: m.ProductIdsInOrder = this.Data.ProductIdsInOrder;
35: }
36: ));
37: this.MarkAsComplete();
38: }
39: }
40: }
And that brings us to…
Saga persistence
We already saw why Shipping needs to be able to look up its internal sagas using data from the events, but what that means is that simple blob-type persistence of those sagas is out. NServiceBus comes with an NHibernate-based saga persister for exactly this reason, though any persistence mechanism which allows you to query on something other than saga Id would work just as well.
Let’s take a quick look at the saga data that we’ll be storing and see how simple it is:
1: public class ShippingSagaData : ISagaEntity
2: {
3: public virtual Guid Id { get; set; }
4: public virtual string Originator { get; set; }
5: public virtual Guid OrderId { get; set; }
6: public virtual Guid CustomerId { get; set; }
7: public virtual List<Guid> ProductIdsInOrder { get; set; }
8: public virtual bool CustomerHasBeenBilled { get; set; }
9: }
You might have noticed the “Originator” property in there and wondered what it is for. First of all, the ISagaEntity interface requires the two properties Id and Originator. Originator is used to store the return address of the message that started the saga. Id is for what you think it’s for. In this saga, we don’t need to send any messages back to whoever started the saga, but in many others we do. In those cases, we’ll often be handling a message from some other endpoint when we want to possibly report some status back to the client that started the process. By storing that client’s address the first time, we can then “ReplyToOriginator” at any point in the process.
The manufacturing sample that comes with NServiceBus shows how this works.
Saga Lookup
Earlier, we saw the need to search for sagas based on order Id. The way to hook into the infrastructure and perform these lookups is by implementing “IFindSagas<T>.Using<M>” where T is the type of the saga data and M is the type of message. In our example, doing this using NHibernate would look like this:
1: public class ShippingSagaFinder :
2: IFindSagas<ShippingSagaData>.Using<OrderAccepted>,
3: IFindSagas<ShippingSagaData>.Using<CustomerBilledForOrder>
4: {
5: public ShippingSagaData FindBy(CustomerBilledForOrder message)
6: {
7: return FindBy(message.OrderId)
8: }
9:
10: public ShippingSagaData FindBy(OrderAccepted message)
11: {
12: return FindBy(message.OrderId)
13: }
14:
15: private ShippingSagaData FindBy(Guid orderId)
16: {
17: return sessionFactory.GetCurrentSession().CreateCriteria(typeof(ShippingSagaData))
18: .Add(Expression.Eq("OrderId", orderId))
19: .UniqueResult<ShippingSagaData>();
20: }
21:
22: private ISessionFactory sessionFactory;
23:
24: public virtual ISessionFactory SessionFactory
25: {
26: get { return sessionFactory; }
27: set { sessionFactory = value; }
28: }
29: }
For a performance boost, we’d probably index our saga data by order Id.
On concurrency
Another important note is that for this saga, if both messages were handled in parallel on different machines, the saga could get stuck. The persistence mechanism here needs to prevent this. When using NHibernate over a database with the appropriate isolation level (Repeatable Read – the default in NServiceBus), this “just works”. If/When implementing your own saga persistence mechanism, it is important to understand the kind of concurrency your business logic can live with.
Take a look at Ayende’s example for mobile phone billing to get a feeling for what that’s like.
Summary
In almost any event-driven architecture, you’ll have services correlating multiple events in order to make decisions. The saga pattern is a great fit there, and not at all difficult to implement. You do need to take into account that events may arrive out of order and implement the saga logic accordingly, but it’s really not that big a deal. Do take the time to think through what data will need to be stored in order for the saga to be fault-tolerant, as well as a persistence mechanism that will allow you to look up that data based on event data.
If you feel like giving this approach a try, but don’t have an environment handy for this, download NServiceBus and take a look at the samples. It’s really quick and easy to get set up.
|
If you liked this article, you might also like articles in these categories:
If you've got a minute, you might enjoy taking a look at some of my best articles.I've gone through the hundreds of articles I've written over the past 4 years and put together a list of the best ones as ranked by my 3000+ readers. You won't be disappointed. If you'd like to get new articles sent to you when they're published, it's easy and free.Subscribe right here. Something on your mind? Got a question? I'd be thrilled to hear it. Leave a comment below or email me, whatever works for you. 13 CommentsYour comment... |








April 20th, 2009 at 6:36 am
Well written and interesting. Thx.
April 20th, 2009 at 6:44 am
Saga Persistence and Event-Driven Architectures…
Thank you for submitting this cool story – Trackback from DotNetShoutout…
April 21st, 2009 at 12:13 am
The clearest explanation of “Saga” i have ever seen.
Thank you very much.
April 21st, 2009 at 1:56 pm
Thanks for this great post explaining Sagas!
June 10th, 2009 at 12:54 am
Udi you post have been great. Recently I have been refreshing thro an earlier read of NService Bus and Saga.
As I have understood long running process flow(consiciously avoiding the word workflow), effectively implemented in state machine scheme of things constitute a Saga. Saga represented a blueprint of process flow and on how it is supposed to behave in deterministic manner. We could have numerous instance of this blueprint aided by the messaging infrastructure, SagaMessage and handler.
When we go ahead try to implement various such process flow in the form of numerous blueprints of saga library I think the code base could gets voluminous, lot of code and time consuming. Thinking in this direction would it be helpful to make some portion of saga declarative. By declarative part I do not mean config associated but could we think of declarative saga using XAML which self describe the saga in XAML if possible associating declarative saga with programmatic saga.
If that would be task, I would like to understand, from you, thoughts on what it would take to do such a thing.
June 10th, 2009 at 12:26 pm
Raghunath,
There is some interesting some going on in declarative saga declaration in the Mass Transit project – that is under consideration for inclusion in nServiceBus.
“I think the code base could gets voluminous, lot of code and time consuming”
What I’ve found is that these sagas don’t just pile up but belong in the context of higher-level business services and bounded contexts. This partitioning prevents things from getting out of hand.
About the “time consuming” parts, well, it’s really not. The fact that the code is isolated from most everything else, as well as being highly unit-testable (with the NServiceBus.Testing library), removes most obstacles from getting it functional and stable – performance and scalability are handled “by default” by the messaging aspects around it.
Does that answer your questions?
June 11th, 2009 at 5:10 am
Udi,
I see the direction saga would move towards.
I may be vague in what I say, I was just thinking if some thing like what is said this post http://blog.pixelingene.com/?p=32
is possible with saga using concepts such as
– Saga
– Initial Trigger
– Event Messages
– Transitions
– Handler
and linking programmatic saga thro XAML Extensions which save you lot of hand-written code improve readility.
I also see WF4 also being more declarative.
Thanks,
Raghunath
June 12th, 2009 at 3:05 pm
Raghunath,
Not all sagas have very much state-management to them.
Many are just simple integration pieces.
I have no direct problem with things being declarative, it’s just that that’s not an end unto itself, but a means that does not make sense in all cases.
Hope that makes sense.
June 18th, 2009 at 7:50 am
After sending my last post I realized that the object which is receiving the event notification will likely not have access to any context other than the domain object itself. So, if the domain object (in this case the Customer) does not have an associated list of Email domain objects there may not be a way to add an Email domain object into the context of the Customer or to hold on to an email request and have it sent as part of (or directly after) the context in which the Customer object exists is serialized.
I hope that this apparent problem is just my lack of understanding rearing its head and not something which is difficult to overcome.
Thanks again,
Andy
September 21st, 2009 at 6:57 am
[...] features in the technology of our choice, and the subject of this post regards extending the Saga Persister. However at this point let me just say that it may well be worth your while to read Udi’s post on [...]
September 27th, 2009 at 4:33 am
[...] is a dll called “NServiceBus.Testing” which provides us with the ability to easily test your sagas. This enables us to move forward with greater confidence in knowing that the code that we have [...]
November 2nd, 2009 at 7:24 pm
[...] gives a much better explanation of them here and there is some more information about them on the NServiceBus Wiki [...]
December 7th, 2009 at 5:52 am
[...] how to use the extensibility points provided by NServiceBus to create a persistence mechanism for Sagas. Notice I said two persisters, one for LINQ To SQL and another for the Entity Framework, this post [...]