Event Collaboration
Multiple components work together by communicating with each other by sending events when their internal state changes.
19 June 2006
This is part of the Further Enterprise Application Architecture development writing that I was doing in the mid 2000’s. Sadly too many other things have claimed my attention since, so I haven’t had time to work on them further, nor do I see much time in the foreseeable future. As such this material is very much in draft form and I won’t be doing any corrections or updates until I’m able to find time to work on it again.
When we have components that collaborate with each other, whether they be as small objects in a single address space or as large as application communicating across the Internet, we commonly think of their style of collaboration as being driven by requests. One component needs information that another has, so the needier component requests it, if then that component needs another to do something, out goes another request.
Event Collaboration works differently. Instead of components making requests when they need something, components raise events when things change. Other components then listen to events and react appropriately. Event Collaboration leads to some very different ways of thinking about how parts need to think about their interaction with other parts.
How it Works
As with many things the easiest place to begin is with an example. Let's set it in my home in 2006 as it was probably imagined in the 70's. As such when I want to pop out I tell my house computer, which has a silly name like Zen, and considers the outside temperature and tells my robot valet to fetch a down jacket for the cold New England winter's day.
Figure 1: Collaboration using requests
Sequence diagrams illustrate the difference quite well. Figure 1 uses the request collaboration style. When I tell Zen I'm going out it queries the temperature sensor for the temperature, uses this information to figure out what coat I'll need and tells the valet to get the down jacket.
Figure 2: Collaboration using events
There are two elements to the collaboration here: the query Zen issues to the temperature sensor and the command Zen issues to the valet. Queries and commands are different (although they can be combined) and result in different effects on the communication patterns.
The Event Collaboration of Figure 2 works differently and although the example has the usual whiff of an author's simplification I think it serves to bring out some of the important differences between these two styles.
One obvious difference is that the events are sent to everyone else, not just those components that are going to react. The point here is that the sender is just broadcasting the event, the sender does not need to know who is interested and who will respond. This loose coupling means that the sender does not have to care about responses, allowing us to add behavior by plugging new components onto the event bus. This flexibility has it's pros and cons, as we shall see.
Commands
The differences in the collaboration style manifest themselves in different ways for commands and queries, so I'll look at them separately and in reverse order. In many ways the command is the less altered of the two interactions, there's still a need to tell the valet to do what it needs to do. One difference is the way the event is named. Rather than phrasing it the form that says it's telling the recipient to do something, it takes the form of saying that an event as happened - inviting those interested to respond. It's a subtle difference, and brings to mind the way my mother used to issue commands, but there is something there. At times the distinction is little more than phrasing, but at other times it does open the mind to different ways of doing things.
The 'unknown' recipients aspect of events now raises its head. What if the valet has been told by Cindy replace the broken leg on the antique library chair. One presumes that the valet is sufficiently intelligent to know that her commands are far more important than mine, but how does Zen react to this? One could add further protocol with events for accepting the task and finishing the task, for instance. In any case there is an implied responsibility here that if Zen wants to be really sure the job is done, it had better ensure someone agreed to carry out the command. Just raising an event is not for a command, you have to know something's going to act.
A related problem occurs if we splash out and get a second valet. Which one should respond to the event? In a true decentralized, event driven world one would assume that the valet's would figure it out between themselves, but I can't rid myself the thought of them fighting over my poor jacket both desperate for the pleasure jolt the programmers set them to receive when I say 'thank you'.
Even in a world of event collaboration, there is a role for a centralized decision of who will carry out commands. So Zen could broadcast the event as “A desire has occurred for Valet George to get Martin's jacket”. My mother would find this far too direct. In practice this means that the difference between events and commands can easily fade into the mist, just remember that it doesn't always do that.
Queries
The more interesting change occurs with queries. You'll notice that in the event collaboration case, Zen never asks the temperature sensor for a temperature. Instead the temperature sensor broadcasts the temperature. Usually this kind of broadcast is done when the temperature changes, but it might also be on a regular schedule.
The idea is that when the temperature is broadcast, then components that need that data make a note of it. When I tell Zen I'm going out, it doesn't ask the temperature sensor for the temperature because it's kept a note of the last event and thus knows the temperature. I find this the most interesting difference in event collaboration, to paraphrase Jon Udell: request driven software speaks when spoken to, event driven software speaks when it has something to say.
A consequence of this is that the responsibility of managing state shifts. In request collaboration you strive to ensure that every piece of data has one home, and you look it up from that home if you want it. This home is responsible for the structure of data, how long it's stored, how to access it. In the event collaboration scenario the source of new data is welcome to forget the data the second it's passed to its Message Endpoint.
As a result the data has to be stored by its user, in this case Zen. If Zen needs a record of past temperatures, perhaps to guess how things might change as I go out, then it's up to Zen to keep that history. Similarly it's up to Zen what data to keep and how to structure it. Zen is free to throw away anything in the temperature it doesn't need - not that a single value is much of an example for that, but clearly useful if you're sending out richer records of information.
A good example of this difference are the siblings of XML processing: SAX and DOM. DOM uses request collaboration, you tell it to load up a document and then issue requests to it to get information. With SAX you listen to various events as the parser reads the data source. DOM holds data, but SAX is stateless - you have to choose what state to hold. The two result in a very different programming model. I find DOM far more convenient when my processing of elements depends a lot on the element's context - but SAX is better when I don't need that context. SAX is also far less overhead and often operates more quickly due to this.
Another consequence of this is that the data's immediately replicated. Every user of the data keeps his own copy. For many designers data replication is a terrible evil. After all if you have multiple sources of data you're like the man with two watches who never knows what time it is. But this is a very controlled replication. You only have one watch and it's constantly monitoring the ether to detect when things change. Of course if a connection is down it will miss something, but then in a request scenario you can't issue a query without a connection to the source. An event queue will usually ensure that messages wait until you come back online and then deliver them to you.
You can still control changes to the data to ensure there's only one source for each bit of data. However instead of doing this by saying only one component stores data, you do it by allowing one component to post update events about that bit of data.
So if you imagine a scenario where you want all your customer data to be managed by a central customer management application. In particular it manages the customer addresses. You also have a frequent imbiber program that rewards people who believe that its important to keep their single malt tasting skills up by practical means. When I use the frequent imbiber website to order a special offer bottle of Lagavulin it sends a query to the customer management system to get my shipping address during my session with the website. If during this session I wish to update my address, the frequent imbiber application will take my request and send a command message to the customer management system to do the update.
If we take this scenario to an event driven world, now all applications that need customer data keep their own, read-only, copies of the customer data that they need. So during my order for the Islay nectar the frequent imbiber application only consults its own replica of customer addresses (it would only need to store addresses for people in the frequent imbiber program). If I request a change of address, the scenario is pretty much the same as the request driven case. The frequent imbiber app sends a command message to the customer management application. The only difference is that the customer management application then posts an event saying my address has changed which will then cause the frequent imbiber application to update its record of my address.
This example brings out the point that if you want central management of data, you'll need two kinds of event - requests to update data, which should be ignored by everything but the managing application, and confirmed updates which everyone acts on. This may also lead you to using a separate channel for update requests.
Event Cascade
When you're using Event Collaboration you need to be aware the consequences of cascades. Event cascades are seen by some as the monster in the lake, others delight in them.
An event cascade is what happens when you get a sequence of events triggering other events. Unusually I'll illustrate this with an abstraction. Imagine three events A, B, and C. When you are working on event A you can comprehend that B is a consequence (A -> B) and that is good. Someone else is working on B and can comprehend that C is a consequence (B -> C) and that is good. However there is now a cascade of A -> B -> C, which may be hard to see because you need to bridge both contexts to see it; while you're thinking of A you don't think about C and while you're working on B you don't think of A. . As a result this cascade can result in unexpected behavior which may be a good thing, but also can be a problem. A three step cascade like this is the simple case, but obviously cascades can become arbitrarily long - and the longer they are the more surprising they can be.
Let's looks at a rather more realistic example. Consider a management system for operating rooms in a hospital. Operating rooms are scarce and expensive resources, so they have to managed carefully to get the most use out of them. Operations are booked in advance, sometimes weeks in advance (such as when I had the pins taken out of my healed arm), sometimes rather more rapidly (such as when I had my broken arm in the first place where I was operated on within hours).
Since operating rooms are scarce resources it makes sense that if an operation is canceled or postponed the room should be released so that it can be scheduled for something else. So the room scheduling system should listen for operation-postponed events and release the room where the operation was scheduled to take place.
When a patient comes in for a procedure, the hospital will run a series of pre-op evaluations. If one of these evaluations contra-indicates the operation, the operation should be postponed - at least until a clinician has a chance to look at it. So an operation scheduling system should listen for pre-op contra-indications and postpone any operation that is contra-indicated.
Both of these event causations are sensible, yet together they can have an unintended effect. Should someone come in and get a contra-indication, the operation is postponed and the room released. The problem is that a clinician might review the pre-op evaluations within a few minutes and decide to go ahead anyway, but in the meantime the room got booked by another operation. This can be very inconvenient to the patient, who even for a straightforward operation would have to prepare and fast for the operation.
Event cascades are good because something happens and as a result of a string of local logical event connections something indirect happens. Event cascades are bad because something happens and as a result of a string of local logical event connections something indirect happens. Event cascades usually look pretty obvious when they are described like this, but until you see them they can be very hard to spot. This is an area where visualization systems that can build a graph of event chains by querying meta-data from the systems themselves could be very handy.
When to Use It
The great strength of Event Collaboration is that it affords a very loose coupling between its components; this, of course, is also its great weakness.
With Event Collaboration you can easily add new components to a system without existing components needing to know anything about the new arrivals. As long as the newcomers listen to the events, they can collaborate.
Event Collaboration helps keep each component simple. All they need to know of the world is the events they listen to. Whenever anything interesting happens they emit an event - they don't even need to care if anyone else is listening. This allows developers to focus on one component at a time - a component with very well controlled inputs.
Event Collaboration is a great environment for Event Sourcing. If all communication uses Event Collaboration then that removes the need for an event sourced application to use gateways on its inputs to mimic event communication.
A system that uses Event Collaboration is more resilient to breakdowns. Since each component has all it needs to operate it can continue working even if communication to the outside world is lost. But this is a two-edged sword. A component may continue operating, but it will be working on out of date information if it's not receiving events as things change. It may therefore initiate actions based on out of date information. With request collaboration, it would just not work - which in some scenarios may be preferable.
Although each individual component is simpler, the complexity of the interactions will increase. This is made worse because these interactions are not clear by reading the source code. With request collaboration it's easy to see that a call from one component leads to a reaction from another component. Even with polymorphism it's not difficult to look things up and see the results. With Event Collaboration you don't know who is listening to the events until run-time - which in practice means you can only find the links between the components in configuration data - and there may be multiple areas of configuration data. As a result these interactions are hard to find, understand and debug. It is very helpful here to use automated tools that can display the configuration of components at run time so you can see what you have.
Since each participant stores all the data that it needs, a lot of data will get replicated. This may not be as much as you think, since systems only need to store the data they need, so will only take a subset. Events themselves will also need to be stored, to act as an audit trail, help with error recovery, and the like. With very large datasets this may be an issue - although storage costs in particular are declining faster than most other things these days.
Acknowledgements
My colleague Ian Cartwright helped he a great deal with his experience with this pattern. Doug Marcey helped me with a realistic example on event cascades.
Example: Trading (C#)
We decided to use a stock trading example to help illustrate Event Collaboration and how it differs from the request-response style. We'll start with a base example and highlight how a couple of modifications to the system work differently due to the collaboration style.
The basic setup we begin with is that we have traders who make trades, which are then sent to a stock exchange. These trades are unconfirmed until the exchange notifies that they have been executed. Here's a simple test for this:
[Test] public void NarrativeOfOrderLifeCycle() { traderA.PlaceOrder("TW", 1000); Assert.AreEqual(1, traderA.OutstandingOrders.Count); ExecuteAllOrders(); Assert.AreEqual(0, traderA.OutstandingOrders.Count); }
To show how this works in the two styles we'll explore both styles of interaction as C# in-memory objects. The same principles would apply with multiple machines operating over a network. In each case we'll have multiple trader objects and multiple stock exchange objects.
We'll start with how this might work in a request/response style. There are two interactions we have to consider: placement and execution. For placement the trader object needs to tell the stock exchange that the trade has been made.
class Trader...
public void PlaceOrder(string symbol, int volume) { StockExchange exchange = ServiceLocator.StockExchangeFor(symbol); Order order = new Order(symbol, volume, this); exchange.SubmitOrder(order); }
We're assuming here that each stock is traded in a single exchange and we find the correct exchange via a look-up. Our Orders are very simple, all we need is a symbol, a volume, and the trader who made it.
The stock exchange object reacts by simply adding the new order to its list of outstanding orders.
class StockExchange...
public void SubmitOrder(Order order) { outstandingOrders.Add(order); } private List<Order> outstandingOrders = new List<Order>();
Submitting the order to the stock exchange object is an example of a command operation. We tell the exchange what we want to do, we don't really care about any results unless anything goes wrong (in which case we get an exception). The command is also synchronous, in that we wait for the stock exchange to answer before we continue processing. That answer may only mean the exchange has received the submission, it may mean validation has occurred. Since we've provided a reference to the caller (in the order) the exchange may later contact the trader about the order.
Now let's look at the code for execution.
class StockExchange...
public void Execute(Order o) { outstandingOrders.Remove(o); }
As far as the exchange object all it needs to do is to remove the order from its list of outstanding orders as a record that the real exchange did whatever it needed to do. Should a trader need to see what orders were outstanding it does so by asking the exchanges.
class Trader...
public List<Order> OutstandingOrders { get { List<Order> result = new List<Order>(); ServiceLocator.ForEachExchange(delegate(StockExchange ex) { result.AddRange(ex.OutstandingOrdersFor(this)); }); return result; } }
Figure 3: Sequence diagram summarizing the interactions for placement, executions, and trader query.
Now lets look at this same scenario using Event Collaboration, starting again with placement.
class Trader...
public void PlaceOrder(string stock, int volume) { Order order = new Order(stock, volume); outstandingOrders.Add(order); MessageBus.PublishOrderPlacement(order); }
A first thing to note here is that this time the trader keeps a note of what orders are outstanding - this is part of the shift in responsibility for keeping state. Since the trader needs some state, it's up to the trader to keep that state.
The second shift is the nature of the outward communication to the stock exchange. Here the trader notifies a general message bus, which can pass the event onto anyone who is interested. For the stock exchange to see the event it needs to subscribe to that kind of event, which it does in its constructor.
class StockExchange...
public StockExchange() { MessageBus.SubscribeToOrders(delegate(Order o) { orderReceivedCallback(o); }); }
class MessageBus...
public static void SubscribeToOrders(OrderPlacedDelegate method) { instance.OrderPlaced += method; } public event OrderPlacedDelegate OrderPlaced; public delegate void OrderPlacedDelegate(Order order);
Here we've wrapped the C# eventing mechanism inside publish and subscribe methods. This isn't strictly necessary, but we feel it makes it easier for non-C# people to follow.
When the trader publishes the placement event, the message bus then just pushes that information out to subscribers.
class MessageBus...
public static void PublishOrderPlacement(Order order) { if (instance.OrderPlaced != null) { instance.OrderPlaced(order); } }
class StockExchange...
private void orderReceivedCallback(Order order) { if (orderForThisExchange(order)) outstandingOrders.Add(order); }
Now lets look at execution. This begins again with the stock exchange object.
class StockExchange...
public void Execute (Order o) { MessageBus.PublishExecution(o); outstandingOrders.Remove(o); }
Here it removes the order from its list of outstanding, as it does in the request/response case. However it also publishes an event, which is picked up by the trader.
class Trader...
public Trader() { MessageBus.SubscribeToExecutions(delegate(Order o) { orderExecutedCallback(o); }); } private void orderExecutedCallback(Order o) { if (outstandingOrders.Contains(o)) outstandingOrders.Remove(o); }
(We've spared you the publish/subscribe implementation in the message bus, it's just the same as the previous case.)
The trader now updates its internal state in response to the events from the stock exchange. So to determine outstanding orders, it just checks its own internal state rather than issuing queries to other objects.
class Trader...
public IList<Order> OutstandingOrders { get{ return outstandingOrders.AsReadOnly();} }
Figure 4: Sequence diagram summarizing the interactions for placement, executions, and trader query using events.
Adding a risk tracking component
So far we've looked at the different ways our trader and stock exchange collaborate with these two styles. We can further see the differences by seeing how adding a third component is different in the two styles.
The third component we'll add is a risk management component. It's job is to look at the total volume outstanding across all traders with a particular stock.
class StockRrRiskTester...
[Test] public void ShouldTrackTotalOutstandingVolumeForOrders() { RiskTracker tracker = new RiskTracker(null); traderA.PlaceOrder("TW", 1000); traderA.PlaceOrder("ICMF", 7000); Trader traderB = new Trader(); traderB.PlaceOrder("TW", 500); Assert.AreEqual(1500, tracker.TotalExposure("TW")); }
Furthermore the risk management system needs to send out an alert message if the total outstanding goes above a preset limit, and cancel alerts should it fall below.
class StockRrRiskTester...
[Test] public void ShouldTrackAlertWhenOverLimitForStock() { string symbol = "TW"; ServiceLocator.RiskTracker.SetLimit(symbol, 2000); traderA.PlaceOrder(symbol, 2001); Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent); }
In order for this to work the risk management system needs to be involved in both the placing of orders (which increases exposure) and the execution of orders (which reduces it).
We'll start by looking at the request/response scenario, starting with placement. We've chosen to modify the stock exchange object's submit method to alert the risk manager with each order submission.
class StockExchange...
public void SubmitOrder(Order order) { outstandingOrders.Add(order); ServiceLocator.RiskTracker.CheckForAlertsOnOrderPlacement(order.Stock); }
We could make this change on the trader, but we chose the exchange because it will need this dependency anyway for execution, so we're able to avoid making a dependency from the trader to the risk tracker.
In order for the risk tracker to determine the total exposure, it issues a query against the relevant stock exchange.
class RiskTracker...
public int TotalExposure(string symbol) { return ServiceLocator.StockExchangeFor(symbol).OutstandingVolumeForStock(symbol); }
So when told about a new placement, the risk tracker tests the result of this query against the limit that it holds.
class RiskTracker...
public void CheckForAlertsOnOrderPlacement(String symbol) { if (OverLimit(symbol)) gateway.GenerateAlert(symbol); alertedStocks.Add(symbol); } private bool OverLimit(String symbol) { return stockLimit.ContainsKey(symbol) && TotalExposure(symbol) > stockLimit[symbol]; }
When a trade is executed, again the risk tracker needs to be called by the stock exchange. The tracker again checks to see the current total exposure and determines whether it should cancel the alert.
class StockExchange...
public void Execute(Order o) { outstandingOrders.Remove(o); ServiceLocator.RiskTracker.CheckForCancelledAlerts(o.Stock); }
class RiskTracker...
public void CheckForCancelledAlerts(String symbol) { if (alertedStocks.Contains(symbol) && !OverLimit(symbol)) gateway.CancelAlert(symbol); }
Figure 5: Sequence diagram that shows how placing an order that goes above the risk limits triggers an alert using request/response.
Now let's take a look at the case where we are using Event Collaboration. A key difference is that adding a risk tracker does not require us to make any changes to the existing components. Instead we build the risk tracker to listen to the events that the existing components publish.
class RiskTracker...
public RiskTracker() { stockPosition = new Dictionary<string, int>(); stockLimit = new Dictionary<string, int>(); MessageBus.SubscribeToOrders(delegate(Order o) { handleOrderPlaced(o); }); MessageBus.SubscribeToExecutions(delegate(Order o) { handleExecution(o); }); }
The tracker also needs data structures to keep track of positions, as it won't query the stock exchange in order to determine the exposure.
When a trade is placed, the tracker picks up the event. In response it updates its copy of the positions and checks for going over the limit.
class RiskTracker...
private void handleOrderPlaced(Order order) { if (!stockPosition.ContainsKey(order.Stock)) stockPosition[order.Stock] = 0; stockPosition[order.Stock] += order.Volume; checkForOverLimit(order); } private void checkForOverLimit(Order order) { if (OverLimit(order)) MessageBus.PublishAlertPosted(order); } private bool OverLimit(Order order) { return stockLimit.ContainsKey(order.Stock) && stockPosition[order.Stock] > stockLimit[order.Stock]; }
To continue to use Event Collaboration, it raises an event when an alert appears. If we then want to send an email in response we can write an email gateway to listen to that event.
On execution events, it again updates its copy of the positions and checks to see if it needs to raise a cancellation.
class RiskTracker...
private void handleExecution(Order o) { bool wasOverLimit = OverLimit(o); stockPosition[o.Stock] -= o.Volume; if (wasOverLimit && !OverLimit(o)) MessageBus.PublishAlertCancelled(o); }
Figure 6: Placing an order triggers an alert using events.
This modification brings out some important differences between the two interaction styles.
- We didn't need to modify existing components for Event Collaboration while we did for Request-Response Collaboration. It's worth noting that this was in large part because the existing components broadcast all relevant change information
- We didn't add any dependencies to the new component for Event Collaboration.
- The Event Collaboration risk tracker was able to determine alerts without further communication with other components since it already contained the necessary state. This lowered the amount of inter-component calls - which could be important if the components were in separate processes. You could achieve the same effect with Request-Response Collaboration by passing the current total exposure as part of the call - but this mean the stock exchange object needs to know what data the risk tracker needs to do its work - a further coupling between the two components.
- As we added the risk tracker, we ensured we used events to broadcast it's newly added information so that it could be a good citizen in the general collaboration. (And indeed we used it to trigger the email gateway.) Note that the events published by the risk tracker weren't tied to changes in state of the tracker (as we didn't broadcast the changes to positions) but to the new information that the tracker added to the global knowledge - whether or not the exposure was over the limits.
Many people would consider that these differences are fundamentally about a system with less coupling for Event Collaboration than for Request-Response Collaboration. We're not so sure. In Request-Response Collaboration components are coupled to each other through their interfaces, expressed as a menu of possible requests. But the same basic coupling exists in Event Collaboration, it's just that the interfaces have changed from request calls to events. If I change a component's event, it will still have repercussions on other components.
The crucial difference is that the communication flows between the components are no longer contained inside the components. Using Event Collaboration we don't have to get the stock exchange to tell the risk tracker when it should check its alerts. This behavior occurs implicitly through the evening model.
But this implicit behavior brings with it a dark side. Let's imagine another change to our components. So far we've assumed that orders are executed in total. Let's alter our stock exchange to handle partial execution.
[Test] public void ShouldCancelAlertIfParitalExecutionTakesBelowLimit() { StockExchange exchange = ServiceLocator.StockExchangeFor("TW"); ServiceLocator.RiskTracker.SetLimit("TW", 2000); traderA.PlaceOrder("TW", 3000); Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent); Order theOrder = exchange.OutstandingOrders[0]; exchange.Execute(theOrder, 1000); Assert.AreEqual(1, ServiceLocator.AlertGateway.CancelsSent); } [Test] public void ShouldNotCancelAlertIfParitalExecutionStillAboveLimit() { StockExchange exchange = ServiceLocator.StockExchangeFor("TW"); ServiceLocator.RiskTracker.SetLimit("TW", 2000); traderA.PlaceOrder("TW", 3000); Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent); Order theOrder = exchange.OutstandingOrders[0]; exchange.Execute(theOrder, 999); Assert.AreEqual(0, ServiceLocator.AlertGateway.CancelsSent); }
Now lets look at the modifications we need to make to do this. For Request-Response Collaboration, we need to modify the stock exchange to record a partial execution.
class StockExchange...
public void Execute(Order o, int volume) { o.ExecutedAmount += volume; if (o.IsFullyExecuted) outstandingOrders.Remove(o); ServiceLocator.RiskTracker.CheckForCancelledAlerts(o.Stock); } public void Execute(Order o) { Execute(o, o.Volume); }
class Order...
public int ExecutedAmount { get { return executedVolume; } set { executedVolume = value; } } public bool IsFullyExecuted { get { return executedVolume == volume; } } private int executedVolume;
Because there's a reference to the risk tracker right there, this helps remind us to consider to modify the risk tracker to take into account the partial execution case. However since the tracker gets the total outstanding by querying the stock exchange, we don't actually have to modify the risk tracker at all.
When we look at the Event Collaboration case, things are a bit more tricky. The basic modification to the stock exchange object is similar.
class StockExchange...
public void Execute(Order o, int amount) { o.ExecutedAmount += amount; MessageBus.PublishExecution(o); if (o.IsFullyExecuted) outstandingOrders.Remove(o); } public void Execute(Order o) { Execute(o, o.Volume); }
However since there's no indication of the link to the risk tracker, there's nothing to suggest that we should consider modifying it. Since the executed amount is a property of the order, everything will still compile cleanly, even with a statically typed system. But the risk tracker will now start giving out incorrect information, without any more obvious signs of failure, since it isn't properly capturing the data changes.
This, of course, is the inherent weakness of any system with implicit behavior - since you can't see it, you don't see what changes need to be made when you modify it. It can also be hard to debug, again because the flow of logic isn't made explicit in code.
What this shows is that using Event Collaboration makes some changes easier, but others harder. It's not easy to get a fair picture of the trade-offs, since we haven't seen much that really captures experience based on a real understanding of the two different styles.