Agreement Dispatcher
Structure a processor for Domain Events around business agreements.
11 March 2005
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.
In a system driven by Domain Events its important to be able to easily find and change the processing rules that react to the events. Since events often appear later than they occurred, you must also retain the ability to process old events with old rules.
A Agreement Dispatcher structures an event processor primarily around the business agreement, since the agreement is a common form of organizing variation in the responses to events. By organizing processors by event type using Temporal Property you can handle changing event rules in a controlled manner.
How it Works
An Agreement Dispatcher is a way of modularizing a processor for Domain Events so that the dominant module is lined up with a business agreement. I use the term 'agreeement' rather than 'contract' partly because the agreements are often not formal contracts and partly to avoid collision with the notion of software contracts in Design by Contract.
My discussion of Agreement Dispatcher follows the principle of Delegated Dispatch . You model each agreement as an object which has a method to process an event. The actual processing code is placed in separate processor objects that are loaded into the agreement in a structured manner. The essential behavior of the agreement is to find the correct processor and then use it to process the event.
This leads to the question of what the correct structure is. Naturally this is to some extent dependent upon your problem domain, but one structure I've seen several times which works well is to structure processors by event type and time.
(If you're not used to Temporal Property this may seem rather tricky. Essentially think of it as the agreement containing a hash map whose keys are event types and whole values are temporal collections of processors. A temporal collection is a special collection that is an implementation of Temporal Property.)
The value of using Temporal Property here is that it allows you to store processing rules that vary over time, and still be able to process events with old rules.
A shipping company decides to increase its charge from $10 to $15 on March 14th. On March 22nd it receives an event saying a shipment was made on March 7th. It's important that it processes this event using the rules that were valid on March 7th (a $10 charge) rather than the rules that are valid on the day it processes the event.
Inheritance is a natural concept for structuring agreements. Don't use the inheritance in the programming language for this, instead give the agreement a property for the parent. Set up the processing code so that if there isn't a rule on the child agreement and it looks for a rule on the parent.
When to Use It
There are two main strengths to Agreement Dispatcher: it's agreement centered nature and the ability to cope nicely with temporal rules.
Using the agreement as the first step in the dispatch delegation makes it easier to handle cases where the logic for handling events varies depending upon the business agreement in place - which is a common scenario.
Dispatching to rules organized in Temporal Propertys is useful because it provides a good structure to cope with changes in rules while allowing old events to be processed with old rules. This is a common cause of messy conditional date logic in systems that lack that facility.
I've described this pattern here because I've seen it work well in a number of cases. However one thing that makes me uncomfortable is that I've not seen much in terms of varations or alternatives. The most common way of doing this kind of event processor is to have little or no structure, I've not seen cases where there has been a good alternative structure.
However the two main reasons for doing this suggest alternative variations. If the primary point of variation for your business processing isn't the agreements, then use that alternative as the central point for your dispatcher. The idea of putting processors in temporal collections to allow rules to be executed at different times can be used in other contexts.
Agreement Dispatcher is an example of an Adaptive Object Model and as such brings in its advantages and disadvantages. It does deal with complexity well (at least within the grain of its variability). It can make someone who is familiar with the model very productive. However it is also hard to learn and can be quite intimidating for the newcomer.
Example: Utility Billing (Java)
A particular combination of patterns that I've run into several times is that of Domain Event, Agreement Dispatcher, and Accounting Entry. In this combination Domain Events are processed by a Agreement Dispatcher which creates Accounting Entrys. This combination works particularly well because Agreement Dispatcher which creates Accounting Entrys are easy to adjust - allowing you to use the generic adjustment strategies to handle adjustment of erroneous information.
To illustrate this I'll use a utility billing example. The billing system recieves various Domain Events and processes them into Accounting Entrys on customer accounts. The model is rather complicated to describe so I'll do it in stages.
- First I'll show the framework classes' structure
- Then I'll show how the you wire the framework classes up
Framework Structure
Figure 2 shows the basic classes involved. Here's how this data structure looks in code.
Figure 2: Classes for utility example
The Accounting Event is a usage of Domain Event which here. Its data is straightforward.
class AccountingEvent...
private EventType type; private MfDate whenOccurred; private MfDate whenNoticed; private Subject subject;
The subject is an interface that defines methods that are needed for the event processor. The only implementation we are interested in is Customer. Customers have two things of interest to us, a service agreement and a set of accounts which will hold the results of our event processing.
class Customer implements Subject
private ServiceAgreement serviceAgreement;
private Map<AccountType, Account> accounts, savedRealAccounts; public Customer(String name) { _name = name; setUpAccounts(); } void setUpAccounts() { accounts = new HashMap<AccountType, Account>(); for (AccountType type : AccountType.values()) accounts.put(type, new Account(Currency.USD, type)); } public Account accountFor(AccountType type) { assert accounts.containsKey(type); return accounts.get(type); } public void addEntry(Entry arg, AccountType type) { accountFor(type).post(arg); } public Money balanceFor(AccountType key) { return accountFor(key).balance(); }
The accounts are stored in a Map, one for each account type.
The data structure in the service agreement is a bit more complicated. Essentially it consists of a Map where the keys are event types and the values are temporal collections of posting rules. I set up this structure dynamically.
class ServiceAgreement...
private Map postingRules = new HashMap(); public void addPostingRule(EventType eventType, PostingRule rule, MfDate date) { if (postingRules.get(eventType) == null) postingRules.put(eventType, new SingleTemporalCollection()); getRulesTemporalCollectionFor(eventType).put(date, rule); } private TemporalCollection getRulesTemporalCollectionFor(EventType eventType) { TemporalCollection result = (TemporalCollection) postingRules.get(eventType); assert result != null; return result; }
The temporal collection class is the implementation of Temporal Property I discussed there.
public interface TemporalCollection { //get and put at a supplied date Object get(MfDate when); void put(MfDate at, Object item); Object get(int year, int month, int date); //get and put at today's date Object get(); void put(Object item); }
Posting rules are simple processors that know about posting an amount to some Account. I set them up by indicating which account they should post to and whether they are taxable.
class PostingRule...
private AccountType type; private boolean isTaxable; protected PostingRule(AccountType type, boolean isTaxable) { this.type = type; this.isTaxable = isTaxable; }
Posting rules are specialized chunks of code which calculate an amount to be charged and then charge it to the given account.
class PostingRule...
public void process(AccountingEvent evt) { makeEntry(evt, calculateAmount(evt)); if (isTaxable) generateTax(evt); } abstract protected Money calculateAmount(AccountingEvent evt); private void makeEntry(AccountingEvent evt, Money amount) { Entry newEntry = new Entry(amount, evt.getWhenNoticed()); evt.getSubject().addEntry(newEntry, type); evt.addResultingEntry(newEntry); }
There are various ways of calculating this amount, and these are left to a subclass. (I also have a mechanism for handling taxes, but I'll defer discussion of that till later. )
Adding a posting rule for usage
So now let's see how we put a single posting rule into this structure.
In a standard agreement, when we receive an event indicating the customer has used some electricity, we make a charge which is the multiple of the usage and rate defined in the customer's service agreement.
To put this simple agreement into place we need to create a service agreement with a posting rule to handle an accounting event that provides information about the electricity usage.
Here is the usage event.
class Usage...
private Quantity amount; public Usage(Quantity amount, MfDate whenOccurred, MfDate whenNoticed, Customer customer) { super(EventType.USAGE, whenOccurred, whenNoticed, customer); this.amount = amount; } public Quantity getAmount() { return amount; }
In order to be able to calculate a charge for the usage we need a posting rule that can multiply the usage by the rate. For this we'll put together a sublcass of posting rule.
class MultiplyByRatePR...
public class MultiplyByRatePR extends PostingRule { public MultiplyByRatePR(AccountType type, boolean isTaxable) { super(type, isTaxable); } protected Money calculateAmount(AccountingEvent evt) { Usage usageEvent = (Usage) evt; return Money.dollars(usageEvent.getAmount().getAmount() * usageEvent.getRate()); } }
class Usage...
double getRate() { return ((Customer) getSubject()).getServiceAgreement().getRate(getWhenOccurred()); }
To complete the exercise we add the new posting rule into a service agreement
class ExampleTester…
private ServiceAgreement simpleAgreement() { ServiceAgreement result = new ServiceAgreement(); result.setRate(10, MfDate.PAST); result.addPostingRule(EventType.USAGE, new MultiplyByRatePR(AccountType.BASE_USAGE, false), new MfDate(1999, 10, 1)); return result; }
Figure 3: Class diagram showing the extra classes needed for our simple example.
Figure 4: Object diagram showing how the posting rule is connected to the agreement.
How the framework executes
So that's how a simple agreement is connected together, now we'll look at how it executes.
In the begining the event data would come in from some external source, which I'll conveniently ignore. Instead I assume that a reader will instantiate a usage event object for me and put it on an event list. We can then process the event list to fire up the event. I can capture this in a JUnit test case.
class ExampleTester…
public void testSimpleRule() { Customer mycroft = new Customer ("Mycroft Homes"); mycroft.setServiceAgreement(simpleAgreement()); AccountingEvent usageEvent = new Usage(Unit.KWH.amount(50), new MfDate(1999, 10, 1), new MfDate(1999, 10, 15), mycroft); EventList eventList = new EventList(); eventList.add(usageEvent); eventList.process(); assertEquals(Money.dollars(500), mycroft.balanceFor(AccountType.BASE_USAGE)); assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.SERVICE)); assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.TAX)); }
Restating what the code says: I create an example customer and assign him the standard agreement I created above. I then create a usage event for mycroft, add it to my event list, and process the event list. The resulting charges are then visible in the Mycroft's accounts.
Executing all this is an exercise in delegation; as Figure 5 shows it's quite a long chain. I start with the event list, all it does is process its unprocessed events.
class EventList...
public void process() { for (AccountingEvent event : unprocessedEvents()) { try { event.process(); } catch (Exception e) { if (shouldOnlyLogProcessingErrors) logProcessingError (event, e); else throw new RuntimeException(e); } } }
If any events fail to process I log and carry on.
For this example I've made the event class be the event processor. Although initially it makes sense to think of the event processor as a separate class, fundamentally the processor does a lot of initimate manipulations on the event - so it makes sense to put the processing behavior on the event itself following the Information Expert princple.
class AccountingEvent...
public void process() { assert !isProcessed; if (adjustedEvent != null) adjustedEvent.reverse(); subject.process(this); markProcessed(); }
Ignore the line about adjustments for now, I'll talk about that more in .
Essentially all the event does is delegate to its subject, which delegates to its service agreement since multiple customers share the same service agreement.
class Customer...
public void process(AccountingEvent e) { serviceAgreement.process(e); }
The service agreement also wants to just delegate, this time to the posting rule. However this time it's a bit more complicated because the service agreement has to determine which rule to use first.
class ServiceAgreement...
public void process(AccountingEvent e) { getPostingRule(e).process(e); } private PostingRule getPostingRule(AccountingEvent event) { final TemporalCollection rules = getRulesTemporalCollectionFor(event.getEventType()); if (rules == null) throw new MissingPostingRuleException(this, event); try { return (PostingRule) rules.get(event.getWhenOccurred()); } catch(IllegalArgumentException e) { throw new MissingPostingRuleException(this, event); } }
The service agreement looks up the posting rule based on the event type and actual time of the event.
Handling More Cases
With this basic framework in place, we can then start extending it to handle more types of event.
Service calls have a cost based fee. On this standard agreement we charge the fee plus 10% of the fee plus $10 for processing.
To add this extra rule all we need to do is add another posting rule to the agreement, the resulting agreement setup looks like this.
class ExampleTester…
private ServiceAgreement simpleAgreement2() { ServiceAgreement result = new ServiceAgreement(); result.setRate(10, MfDate.PAST); result.addPostingRule(EventType.USAGE, new MultiplyByRatePR(AccountType.BASE_USAGE, false), new MfDate(1999, 10, 1)); result.addPostingRule(EventType.SERVICE_CALL, new AmountFormulaPR(1.1, Money.dollars(10), AccountType.SERVICE, false), new MfDate(1999, 10, 1)); return result; }
public class AmountFormulaPR extends PostingRule { private double multiplier; private Money fixedFee; public AmountFormulaPR(double multiplier, Money fixedFee, AccountType type, boolean isTaxable) { super(type, isTaxable); this.multiplier = multiplier; this.fixedFee = fixedFee; } protected Money calculateAmount(AccountingEvent evt) { Money eventAmount = ((MonetaryEvent) evt).getAmount(); return (Money) eventAmount.multiply(multiplier).add(fixedFee); } }
We need a new kind of posting rule for this that supports a simple formula - in this case just multiplying an amount by a multipier and adding a constant.
The other area of variation is that of time.
On Dec 1 the handling fee for service calls goes up to $15
To add this we add another posting rule with the same event type but a different date. So now our agreement definition looks like this.
class ExampleTester…
private ServiceAgreement simpleAgreement3() { ServiceAgreement result = new ServiceAgreement(); result.setRate(10, MfDate.PAST); result.addPostingRule(EventType.USAGE, new MultiplyByRatePR(AccountType.BASE_USAGE, false), new MfDate(1999, 10, 1)); result.addPostingRule(EventType.SERVICE_CALL, new AmountFormulaPR(1.1, Money.dollars(10), AccountType.SERVICE, false), new MfDate(1999, 10, 1)); result.addPostingRule(EventType.SERVICE_CALL, new AmountFormulaPR(1.1, Money.dollars(15), AccountType.SERVICE, false), new MfDate(1999, 12, 1)); return result; }
With this in place service calls before December incur the lower fee but those after get the higher fee.
class ExampleTester…
public void testSimpleRule3() { Customer mycroft = new Customer("Mycroft Homes"); mycroft.setServiceAgreement(simpleAgreement3()); EventList eventList = new EventList(); //service call before rule change AccountingEvent call1 = new MonetaryEvent(Money.dollars(100), EventType.SERVICE_CALL, new MfDate(1999, 10, 1), new MfDate(1999, 10, 15), mycroft); eventList.add(call1); eventList.process(); assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.BASE_USAGE)); assertEquals(Money.dollars(120), mycroft.balanceFor(AccountType.SERVICE)); assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.TAX)); //service call after rule change AccountingEvent call2 = new MonetaryEvent(Money.dollars(100), EventType.SERVICE_CALL, new MfDate(1999, 12, 1), new MfDate(1999, 12, 15), mycroft); eventList.add(call2); eventList.process(); assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.BASE_USAGE)); assertEquals(Money.dollars(245), mycroft.balanceFor(AccountType.SERVICE)); assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.TAX)); }