It’s easy to avoid manually managing transactions when frameworks like Spring and containers do a good job of hiding all the details. However, it’s often more advantageous to take the controls and manage your own transactions. We seem to shy away from this but its really straight forward and if it means we’re not tied into yet another framework, why wouldn’t we? Aside from just avoiding frameworks though, how does replacing @Transctional
with something bespoke really help us?
Moving from a declarative approach to a more imperative one can help us with testing and by virtue; composability. We can move from something which can only be tested using the framework or container (implying an integration or end-to-end style test) to a more focused style (without the need of said frameworks or containers). If we manage things ourselves and are explicit about the transactional boundaries in production code, we can be more lightweight in our tests.
Lets take a look at an example in detail.
It’s probably helpful to be clear what we mean by a unit of work here. Intimately related to the idea of a database transaction, a unit of work is a series of database operations that when applied together adhere to all the transactional characteristics (atomic, coherent, isolated and durable). For example, when updating the database to increment one bank account and decrementing another, things should be atomic (both operations happen or neither does), consistent (the bank accounts actually exist), isolated (protected from concurrent updates to the same accounts) and durable (permanently applied). Describing both operations as a unit of work and applying then transactionally achieves this.
So we can think of the unit of work as something that can be executed and when it is, it’ll be under the conditions described above.
|
Something that would be responsible for executing the unit of work might look like this.
|
When it comes to using Hibernate, we might have a concrete UnitOfWorkRunner
look something like the following. The key thing here is that the transaction management is handled here, its a simple try catch finally pattern and as you can see, is very simple.
|
It’s this class and interface that allows us to be explicit about our transactional boundary. Clients to this define the transaction boundary. In most containers and frameworks, the transactional boundary is around the request/response cycle and the developer has little influence. Using the UnitOfWorkRunner
directly in your code gives more control over this. You can use a servlet filter to achieve a similar request/response scoped transaction or you can be finer grained and produce what I prefer; a transaction scoped to a coherent business operation.
For example, lets have a interface describing current account business functions that work on bank account entities. The CurrentAccount
interface represents business functions and should define the transactional boundary. The BankAccount
on the other hand represents the entities involved which themselves are stored in an Accounts
repostiory.
|
When we implement the CurrentAccount
, we can define the transactional behavior as a separate concern from the business behavior. For example,
|
Where transactionally
is a statically imported creation method that wires up the AcmeBankCurrentAccount
(the business services) with transactional behavior. It does this via decoration but essentially creates an anonymous UnitOfWork
in which to execute the business operation within.
The full class looks like this
|
The underlying business functionality within the AcmeBankCurrentAccount
isn’t concerned with transactions. Instead, its decorated with transactionality and we can use this decorating proxy to wrap any business interface as a transaction.
|
This can come in handy when testing as we can isolate and test the different responsibilities. We’re also left with a handy framework to add ad-hoc data directly to the database and it’s easy enough to wire up an in-memory only UnitOfWorkRunner
. Back to the point earlier about composability, the overall approach leaves us with loosely composed objects which combine to provide high level behavior. The composites are simpler than the sum of its parts to borrow a phrase from GOOS.