architect-handbook

Software Architect Handbook

View on GitHub

Dealing with Transactions

Overview

When breaking apart our databases, maintaining referential integrity becomes problematic, latency can increase, and we can make activities like reporting more compelex.

The ability to make changes to our database in a transaction can make our system much easier to reason about, and therefore easier to develop and maintain. We rely on our database ensuring safety and consistency of our data, leaving us to worry about other things. But when we split data across database, we lose the benefit of using a database transacton to apply changes in state in an atomic fashion.

ACID Transactions

Typically, when we talk about transactions, we are talking about ACID transactions:

ACID but without atomicity?

We could decide to sequence these two transactions; removing a row from the PendingEnrollments table only if we were able to change the row in the Customer table. But we’d still have to reason about what to do if the deletion from the PendingEnrollments table then failed — all logic that we’d need to implement ourselves.

Being able to reorder steps to handle these use cases can be a really useful idea. But fundamentally by decomposing this operation into two separate database transactions, we have to accept that we’ve lost guaranteed atomicity of the operation as a whole.

This lack of atomicity can start to cause significant problems especially if we are migrating systems that previously relied on this property.

Distributed Transactions & Two-Phase Commits

The 2PC algorithm is frequently used to attempt to give us the ability to make transactional changes in a distributed system.

The algorithm is broken into two phases, a voting phase and a commit phase:

We cannot in any way guarantee that these commits will occur at exactly the same time. The coordinator needs to send the commit requests to all participants, and that message could arrive at and be processed at different times. This means it’s possible that that we could see the change made to Worker A, but not yet see the change to Worker B, if we allow for you to view the states of these workers outside the transaction coordinator.

When 2PC work, at their heart they are very often just coordinating distributed locks.

Managing locks, and avoiding deadlocks in a single-process system, isn’t fun. Now imagine the challenges of doing so among multiple participants.

The more participants you have, and the more latency you have in the system, the more issues a 2PC will have. The longer the operations takes, the longer you’ve got resources locked for!

Distributed Transactions - Just Say No!

Whenever possible, you should avoid the use of distributed transactions like the 2PC to coordinate changes in state across your microservices.

The first option could be to just not split the data apart in the first place if you want to manage this data in a truly atomic and consistent way, and you cannot work out how to sensibly get these characteristics without an ACID-style transaction; leave the functionality that manages that state in a single service.

Sagas