All Articles

Adding historization to entities

Keeping a record of your entries
Keeping a nice record of your entries for future lookup

We’ve added some entities in previous articles, and made them belong to users. Most notably, those entities have been bank accounts. That’s great, as users can now enter the balances of their accounts already in our application, as opposed to that silly Excel sheet. However, these balances hardly ever remain constant, as there is usually some movement.

Now, we could simply add an update functionality to our application, where the user can enter the new balance, and he’ll always know how much he’s got, right? Sure, that would work. However, it’d be even nicer if he could actually see the movement of the account over time, to track whether the total is going up, or going down.

Existing Libraries

There are some great, helpful libraries out there that could help us with this. One such library is envers, which is a hibernate plugin and works great on relational databases. I would argue though that envers is not quite what we require, as it is usually used in auditing. We do not require a thorough auditing in our application, as the user can add whatever he’d like. So, we’ll simply create our custom endpoints and functionality to reach an outcome that’s tailored to our needs.

The historical snapshot

Since we’re thinking about what we’d actually like to store first, let’s note the precise information that we’d need.

  • the balance
  • the account it’s a snapshot of
  • the validity timeframe

That’s really already everything that it needs! It doesn’t require the name of the account, as that could change, and has no relevance to this feature. It also doesn’t need the information of the user it belongs to, as we have that information transiently through the account itself. Also, the user will not get access to the history, unless the account actually belongs to him.

That being said, here’s the AccountSnapshot entity:

@Entity
@Table(name = "account_snapshot")
data class AccountSnapshot(
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
    val id: UUID? = null,
    @Column(name = "balance", nullable = false) val balance: Double,
    @Column(name = "valid_from", nullable = false) val validFrom: LocalDate,
    @Column(name = "valid_to", nullable = false) val validTo: LocalDate = LocalDate.now(),
    @ManyToOne(fetch = FetchType.LAZY) val account: Account
    )

Most of it seems pretty self-explanatory. However, some clarifications about the timeline need to be mentioned:

  • validTo is always the day itself, as this is when a new value is added
  • validFrom is based on the last Update of the account
  • the editedOn column needs to be added to the account, as this will serve as the entry for the validFrom in future updates

Next, we have its corresponding entry in the Account:

    @Column(name = "edited_on", nullable = false) val editedOn: LocalDate = LocalDate.now()
    @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true, mappedBy = "account")
    val accountSnapshots: MutableList<AccountSnapshot> = mutableListOf()

Similarly, we have the DTO changes:

data class AccountSnapshotDto(
    val id: UUID,
    val balance: Double,
    val validFrom: LocalDate,
    val validTo: LocalDate = LocalDate.now()
)

data class AccountDto(
...
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    val snapshots: List<AccountSnapshotDto> = listOf()
)

data class BalanceDto(val balance: Double)

Adding the functionality

Great, now we have the domain and dto models all sorted out. Next, we will add the code that will enable the functionality.

Domain object layer

The first layer is logically the Service layer, where changes will happen. However, since I’m a big fan of having the Service mostly be a guide on how to orchestrate functionality, most of the logic is done in the classes itself. So, we need the following operations in the Account:

  • Create the Snapshot
  • Update the balance

Creating the snapshot

This is quite easy, as we have should all the information we need to create a shiny new snapshot: `

    fun createSnapshot(): AccountSnapshot {
        return AccountSnapshot(
            balance = this.balance,
            validFrom = editedOn,
            account = this
        )
    }

Updating the balance

Now, changing the balance is just a little bit more complicated, since the balance is a value, and can therefore not be changed. So, we create a new account with all the same values, except for the balance. This is a particularity of val in kotlin, as in Java, one could simply have made a Setter for the object. In Kotlin, we could technically have done it the same way by declaring the balance with var, but then we could never be sure that this wouldn’t be used in other places also.

    fun updateBalance(balance: Double): Account {
        return this.copy(balance = balance, editedOn = LocalDate.now())
    }

Service layer

Great, now we add the functionality in the Service layer, where the operations look very clean and in order:

    @Transactional
    fun updateAccount(accountId: UUID, balance: Double) {
        val account = getAccount(accountId)
        val snapshot = account.createSnapshot()
        account.accountSnapshots.add(snapshot)
        val updatedAccount = account.updateBalance(balance)
        accountRepository.save(updatedAccount)
    }

For hibernate, it will simply look as though the object has been changed, and it will only perform the changes on that account - not create a new one.

Controller layer

Finally, we expose the endpoint:

    @PostMapping("$ACCOUNT_BY_ID/update")
    fun updateAccount(@PathVariable(ACCOUNT_ID) accountId: UUID, @RequestBody balanceDto: BalanceDto): ResponseEntity<Nothing> {
        accountService.updateAccount(accountId, balanceDto.balance)
        return ResponseEntity.noContent().build()
    }

This is again very simple. The only part I’d like to point out is that it is a POST operation, not a PUT. This is deliberate, as updating the value will create a new item in a database, and not simply update an existing one. It is therefore not idempotent, as a PUT would be.

Testing

After changing the Postman suite we have, and of course the unit tests, both of which I am not going to add here (as they’ve been covered in multiple places throughout other articles), we can see the following response upon a GET after updating the balance. As expected, it contains a new balance, but it has the history in it also!

An account with a history
The account now has a history!

Entity editing

While the next part is not technically part of the historization, it is important also for entity changes.

Typically, an account requires a balance update regularly. The user may want to update his account in different other ways also, though. He may have chosen a name that is not quite what he wants anymore (e.g. SalaryAccount which becomes MainAccount). Or he may want to change the currency, or the description. Whatever the case may be, we should offer it also.

One important item to stress is that here, we do not create a historical entry. The user could technically update his balance in the same operation, but that’s not what this endpoint should be used for. If the user does it, that’s on him. Of course, this should be clearly reflected in the UI that would trigger these endpoints (for example with information bubbles next to both buttons).

Here are the changes, in a similar structure to before.

Domain layer

    fun setImmutableInfoFromAccount(account: Account): Account {
        return this.copy(
            id = account.id,
            accountSnapshots = account.accountSnapshots,
            addedOn = account.addedOn,
            user = account.user
        )
    }

You may note there are quite some custom settings here. It would have been fine to do this the other way, i.e. use the existing account as the basis and copy into the new object the values from the new DTO. The reason I’ve gone this way is that it’s much more likely that new updatable attributes will be added, rather than immutable ones. Therefore, I expect this function to change less as it is now.

Service layer

    @Transactional
    fun editAccount(accountId: UUID, editedAccount: Account) {
        val account = getAccount(accountId)
        val updatedAccount = editedAccount.setImmutableInfoFromAccount(account)
        accountRepository.save(updatedAccount)
    }

Controller layer

    @PutMapping(ACCOUNT_BY_ID)
    fun editAccount(@PathVariable(ACCOUNT_ID) accountId: UUID, @RequestBody accountDto: AccountDto): ResponseEntity<Nothing> {
        accountService.editAccount(accountId, accountDto.toDomain())
        return ResponseEntity.noContent().build()
    }

Again, testing should be added for this functionality also. As expected, the account can now also be edited (but it’s not clear from the results, as we do not see the previous input anymore). You’ll need to trust that I’ve added the appropriate assertions in the other tests.

The edited account
The edited account, with its new values, still has the same history!

There you have it - historization and editing for the account entities. Just one step more to add useful functionality for the user!

Feel free to let me know of other, useful features that these two operations should offer. :)