Since emerging from 16 years in a cave, I’ve discovered this design philosophy called REST. It seems to be popular, although I’m also discovering that there’s a lot of contention around it, and a lot of the descriptions of it seem to miss some of the key points.
I’ve found the following posts to be most helpful in trying to understand the philosophy:
- A Brief Introduction to REST (Stefan Tilkov)
- REST Anti-Patterns (Stefan Tilkov)
- How I Explained REST to My Wife (Ryan Tomayko)
- PUT or POST: The REST of the Story (John Calcote)
Reading all this stuff got me thinking about REST and how to adapt the OO model that many developers carry around in their heads (nouns and verbs). The CRUD model maps pretty well onto REST philosophy (although the mapping has some ambiguity in it depending upon the implementation details), but there are a lot of other verbs out there that we need to handle. The quintessential verb is balance_transfer in a banking application. The breakthrough came when I realized that verbs can be recast as creating a record of the verb, and the record of the verb is itself a noun. Thus, instead of executing the balance_transfer action, instead one should create a balance_transfer record. Creating that record may have side effects (such as updating the balances on the affected accounts). That record may not even be persisted in the application (although I’d hope that it is in the case of a banking application – audit trails are important). But from an API standpoint, one is creating a noun, and that is easy to handle in REST. One way of thinking of this is that in a database with an audit trail that is 100% complete, the current data in the database is simply a persistent cache of the result of committing everything in the audit trail! As a result, one doesn’t need CRUD at all – one only needs CR on the audit trail! Every change to the database is simply the result of creating the appropriate record in the audit trail.
All of this collided with a dream I had several months ago. In the dream I asked a friend what “uninstallable” meant – even in my dream, I’d realized that the word “uninstallable” is ambiguous. It can either mean “unable to be installed” or “able to be uninstalled”! All excited, I ran to my computer and discovered that someone had written a paper on the subject – Hierarchical Morphological Structure and Ambiguity (Carl Vikner). Not only does the paper discuss the ambiguity, but it also brings up a very interesting point. Certain verbs are reversible. For instance, one can unlock a door. One cannot, however, uneat an apple (this is a state change verb, but one cannot undo the state change – the entropy increase is too large) or unyawn (this is a non-state change verb).
So we can divide verbs into three categories:
- No state change (yawn, wag, etc.)
- Reversible state change (install, tie, lock, etc.)
- Irreversible state change (eat, burn, etc.)
In most databases, viewing a record falls into the “no state change” category, but in some databases it falls into one of the latter two categories. For instance, in a medical records database that complies with HIPAA, viewing a record falls into the “irreversible state change” category because it creates a an audit entry to enable investigations of unauthorized access. In a mail reader application, viewing (or reading) an e-mail is a reversible state change – it marks the e-mail as read, but there is generally a “Mark as Unread” action. Note that reversing the action of reading the e-mail has to be referred to by a circumlocution since “unread” is not a valid verb in English!
Which brings up a question. If viewing has side-effects, should one use GET as the verb? GET is supposed to be nullipotent, but if viewing creates an audit trail (which is non-idempotent) or marks the record as read (which is idempotent, but not nullipotent), then is GET the appropriate HTTP method? After some serious thought on this, I’ve concluded the answer is no. Imagine a browser that does aggressive pre-caching of linked pages. It can do that because GET is nullipotent. If, however, viewing records is non-nullipotent, then you want to ensure the browser does not do pre-caching of linked pages. As a result, all of the links for viewing records should be transitioned to using POST. This, unfortunately, makes development a lot uglier, but that’s the price one has to pay for ensuring adherence to the browser model.
The next question that occurred to me is how to handle reversible state changes. For instance, I’m curious how banking systems handle reversing a deposit. There are two ways of handling this – one is to delete the original deposit record (there might be an audit trail somewhere, but in the transaction log for your account the original deposit simply wouldn’t show). The other would be to issue a subsequent “undo deposit” transaction. The latter has the advantage of preserving the original transaction entries as they posted. The former has the advantage of some degree of simplicity. However, in both cases there are potential issues that need to be considered. Imagine I have a balance of $1000 from Jan 1st through Mar 1st. On Mar 1st, an invalid deposit of $999,000 gets made into my bank account. On Apr 1st, I finally notice that my account balance seems a tad high and notify my credit union. They promptly reverse the transaction using either model. However, I still end up with a higher balance than I would have otherwise! Why? Because on Mar 31st, the bank computed interest. In this hypothetical case, let’s assume I get 1% per quarter (wouldn’t that be nice). As a result, I get $3,340 in interest on my average balance of $334,000 for the quarter. If the invalid deposit had never been made, I would have received only $10 in interest. I assume banking applications have ways of handling this – either all balance-dependent transactions following an invalidated transaction are modified, or if an adjustment transaction is inserted on Apr 1st, then adjustment transactions for all of the balance-dependent transactions are inserted as well. Issues can be observed in the reverse case as well – imagine invalid withdrawals being made that result in negative-balance fees being applied to the account. Reversing those invalid withdrawals also needs to clean up the negative-balance fees.