In this series we explore a business architecture that’s more complex than obviously necessary, but where each complication creates value over time.
Watch designers speak in terms of “complications” instead of “features”. I love this framing because it simultaneously reminds everyone of both costs & benefits.
So far we have discussed:
We want the flexibility of human processing with the scale & economy of automated processing.
So we save all data needed to recreate the history of a contract.
But the real world out there & the ideal world of the system diverge.
So we tag all data with both the date on which data became true in the real world & the date on which the system learned about that data (bi-temporality).
Now we come to the next complication: reproduction. We would like to be able to re-run the entire history of a contract from inception to the present moment & get exactly the same results as those we currently have stored (the actual differences will be interesting).
“That fee is outrageous! I’ll only pay half that.” “But that was 5 years ago!” “I don’t care.” Imagine being able to retroactively change the fee, re-run the entire 5 years worth of transactions, & confidently seeing how the balances have changed.
To replay history we must record it.
Accounts & Transactions
Therefore, record monetary flows as transactions between accounts. Bi-temporally stamp these transactions. Implement business logic by reading account balances & posting new transactions.
There is a whole Tao Te Ching 道德經 interpretation of double entry bookkeeping which I’m not going to describe here because I’m no initiate. Feel free to explore.
What accounts do you need? That’s a matter of style, judgement, & iteration. In general, have an account any time a balance is interesting to someone. “How much have I paid in premiums?” Account. “How much risk did we re-insure?” Account. “What is the current street address?” Not an account (but maybe it should be—try it some time).
Implementation: Structure
SubStack is responding differently to indenting than it has before for me. I’m afraid you’ll have to fill in the leading whitespace yourself until I get it figured out.
Here is the basic test we’d like to pass:
monday = 1
tuesday = 2
wednesday = 3
thursday = 4
source = Account()
destination = Account()
Transaction.post(source, destination, 100, tuesdayAsOfTuesday)
mondayAsOfMonday = Perspective(effective=monday, posting=monday)
assert source.balance(mondayAsOfMonday) == 0
tuesdayAsOfTuesday = Perspective(tuesday, tuesday)
assert source.balance(tuesdayAsOfTuesday) == -100
tuesdayAsOfThursday = Perspective(thursday, tuesday)
assert destination.balance(tuesdayAsOfThursday) == 100
Let’s take this top down. The object structure we want to create looks like this:
The external API for posting a transaction seems to fit nicely as a factory method on Transaction:
class Transaction:
@staticmethod
def post(source, destination, amount, perspective):
result = Transaction(source, destination, amount, perspective)
result._post()
return result
@staticmethod
def post(source, destination, amount, perspective):
result = Transaction(source, destination, amount, perspective)
result._post()
return result
This method both creates side effects & returns a new object. Some folks would tsk tsk 🤌🏻. Too bad. Write your own code.
Supporting the factory method are the private __init__ & _post methods:
def __init__(self, source, destination, amount, perspective):
self._source = source
self._destination = destination
self._amount = amount
self._perspective = perspective
def perspective(self):
return self._perspective
def _post(self):
self._source.debit(self)
self._destination.credit(self)
An Account stores 2 Histories of Transactions, one for credits & one for debits. (Real accountants are likely cringing by now—we are using “accounts & transactions” as a metaphor, not implementing genuine double entry bookkeeping.)
class Account:
def __init__(self):
self._credits = History()
self._debits = History()
def credit(self, transaction):
self._credits.add(transaction, transaction.perspective())
def debit(self, transaction):
self._debits.add(transaction, transaction.perspective())
(I went back and forth whether to pull the perspective out here or pass it as an explicit parameter. I decided that this better expressed the invariant that the transaction is always stored under its own perspective.)
Implementation: Behavior
Now that we have the object structure we want, we can implement the balance() method.
class Account:
def balance(self, perspective):
credit = self._balanceOn(self._credits, perspective)
debit = self._balanceOn(self._debits, perspective)
return credit - debit
This relies on a helper that sums the amounts of the visible transactions in a History.
def _balanceOn(self, transactions, perspective):
return sum(map(lambda x: x.amount(), transactions.seen(perspective)))
If you remember History from the last couple of posts, we need an accessor that returns only the seen elements. Here it is for completeness:
class History:
def _seen(self, perspective):
return filter(lambda x: perspective.sees(x[0]), self._events)
def seen(self, perspective):
return map(lambda x: x[1], self._seen(perspective))
(We are inching ever closer to the dreaded range query for History, which I’m really hoping to avoid.)
Conclusion
There you have it. It would seem easy enough to implement some business logic by just querying a bunch of domain objects, writing the results somewhere temporary, summing the results, issuing a report, and then forgetting the intermediate state. This, however, would prevent you from reproducing the current state.
Instead, consider adding the additional complication of storing intermediate business results as transactions in accounts. Implement business logic by querying balances or unprocessed transactions & posting new transactions. Eventually there will be an account like “owed to customer” or “owed to government”. Processing those accounts will cause change in the outside world.
One limitation of this approach is that you can’t (yet?) easily reproduce earlier states of the code. If you process yesterday’s data with today’s logic, you could easily come up with different answers than you did yesterday. In practice this doesn’t come up that often, but it’s interesting to imagine automatic solutions. A bi-temporal code repository? Hmmm…
The ability to replay history may be surprising & novel to the business. Business invented the concept of “closing” (as in “year-end closing”) to sweep the sins of the past under the rug. Retroactive changes become exercises in accounting legerdemain. Don’t expect the business to adapt instantly.
Note that our transactions aren’t the only thing we will need to explain the history of a contract. We also need a Document Store (to be described later) to account for all correspondence sent & received.
Note: In pricing of insurance policies and changes to them, we routinely effective date both the rate data that drives this process *and the code itself.* Especially in government regulated environments (mostly personal insurance). Changes to insurance rates and algorithms must be filed with the government regulating agencies and approved. Policy terms that start after the approved effective date use the new rates and algorithms. Changes to policy terms that start before then use the old data and code.
(Bug fixes can and sometimes must be applied retroactively.)
It's helpful to realize that in modern "stored program" (or Von Neumann) architectures, there is no fundamental difference between code and data. With scripting languages, one can store the script code in effective dated records. In more "conventional" languages, one can effective date the function/method lookup tables.
If I understand well, this is last post of series. I can't find previous/related posts. Or I need to upgrade? Sorry I'm new on this Substack thing.