Discover more from Software Design: Tidy First?
When Did It Happen? When Did We Find Out?
Or, bi-temporality made slightly easier to understand
First, a big shout-out to Massimo Arnoldi & the folks at Lifeware for teaching me these patterns. They were distilled from decades of experience operating tracking let’s just say a lot of money.
“When is she moving? When did you heard about this?” We’ve all heard this line of dialog on TV. Turns out it’s important to business system development. Here’s why.
We can create value by moving some design decisions earlier, in spite of the loss of net present value.
Business architecture is one of these cases.
Combining human judgement & automation gives us flexibility, scalability, & efficiency.
We have to reconcile 2 timelines—what’s going on in the real world out there & what our simplified system model knows about what’s going on, leading us to:
Elephant Principle—remember all business information forever.
Store data tagged by the time we stored it in the system. This lets us answer questions like, “What was the coverage last Tuesday?” (“Point queries”)
So far, so good. The previous model of a Customer let us get and set addresses as of dates.
Now our dual timelines come to bite us. Here’s a scenario:
On Monday we find out that the address is 123 Main.
On Wednesday we find out that the address is now 456 Main. (This customer moves a lot but not far.)
Whoopsie! On Thursday we find out that on Tuesday the customer moved to 789 Main.
We could just say:
customer.address(“789 Main”, tuesday)
Sort of splice the correction into the history. However, we have all of Wednesday’s processing out there based on the wrong address. How can we ask, “What address for Tuesday did I use on Wednesday?”
In other words, we want to record both:
When this piece of information entered the system.
When this piece of information is changed in the outside world.
Our current model conflates the two.
We need to expand the “tags” for business information. Since this tag is a way to look at the system, we’ll call it “Perspective”
def __init__(self, effective, posting):
self.effective = effective
self.posting = posting
def sees(self, other):
return other.posting <= self.posting and other.effective <= self.effective
The key piece of behavior here is that one Perspective only “sees” another if both the posting & effective times (not really just times, but we’ll get to that later) are earlier.
We use Perspective»sees() when we query a History. To find the event corresponding to a Perspective, we
Filter out all the events that can’t be seen.
Sort the visible events by effective date.
Return the most recent.
def get(self, perspective):
visible = filter(lambda x: perspective.sees(x), self._events)
freshest = sorted(visible, key=lambda x: x.effective, reverse=True)
(Recall that events are stored as a tuple of Perspective (was justing posting) & value. All those ’s are starting to bug me so I’ll probably fix that soon.)
Here is the code I used to drive the above implementation.
monday = 1
tuesday = 2
wednesday = 3
thursday = 4
customer = Customer()
mondayAsOfMonday = Perspective(effective=monday, posting=monday)
tuesdayAsOfThursday = Perspective(thursday, tuesday)
assert customer.get_address(mondayAsOfMonday) == "123"
assert customer.get_address(tuesdayAsOfThursday) == "456"
tuesdayAsOfWednesday = Perspective(wednesday, tuesday)
assert customer.get_address(tuesdayAsOfWednesday) == "123"
You might reasonably ask, “So what? These situations are rare.” Yes, but… First, these situations aren’t all that rare. Run a contract for 20 years & things are bound to get wobbly. Second, the cost of not tracking effective & posting separately compounds. Do you just keep running the contract even though it is wrong? Do you hire a room full of expensive actuaries to manually calculate everything? (I’ve seen both strategies used.)
The expected cost of effective/posting mismatches is high. Tracking them both, figuring out the right effective/posting combination, can be tough. It’s what we need to do to keep processing inexpensive & scalable.