How modular can your monolith go? Part 6 - transaction management for commands

architecting   modular monolith   service collaboration  

New public workshop: Architecting for fast, sustainable flow - enabling DevOps and Team Topologies thru architecture. Learn more and enroll.


This article is the sixth in a series of articles about modular monoliths. The other articles are:

In previous articles in this series, I glossed over the issue of transaction management. I simply assumed that each operation (e.g. request) was implemented as a single transaction. For example, the system command createOrder(), which is triggered by an HTTP POST /orders request, would be implemented as a single transaction that creates an Order, updates a Customer to reserve credit, and sends an email to the Customer.

In this article, I dig deeper into the topic of transaction management in a modular monolith. I’ll talk about transaction management when implementing system commands that update multiple domains. I’ll cover three different transaction management strategies:

  • One transaction per operation - simple ACID transactions
  • One transaction per domain - more complex eventually consistent transactions
  • Multiple transactions per operation - carefully designed to avoid some of the complexity of eventual consistency

Let’s start by looking about the need for transactions.

System commands need to be transactional-ish

System operations need to have ACID-style semantics. Specifically, system commands must have the following characteristics:

  • Atomic - all of the updates performed by an operation need to be committed or none of them should be. Partial updates would leave the system in an inconsistent state.

  • Isolated - the outcome of executing multiple operations concurrently (e.g. opA() and opB() ) needs to be the same as if they were executed serially (e.g. either opA() -> opB() or opB() -> opA()).

If an operation doesn’t have these properties then it’s likely to be buggy. For example, if createOrder() did not create Order and update a Customer to reserve credit, then the database would be left in an inconsistent state.

Simplest approach: an operation = ACID database transaction

The simplest solution is to use a relational database and implement each operation as a single ACID transaction. While there are some subtle issues around transaction isolation levels (real world databases don’t implement textbook ACID transactions), this approach is generally simple and easy to implement. The database will ensure that the operations are atomic and isolated (as well as consistent and durable).

ACID transactions = key benefit of the monolithic architecture

The ability to use ACID transactions is one of the key benefits of monolithic architecture. For example, you can simply use the @Transactional annotation in a Spring Boot application to make every operation transactional.

Lack of ACID for distributed operations = key drawback of microservices

Conversely, one of the key drawbacks of microservice architecture is that those operations that are distributed over multiple services must use the eventually consistent service collaboration patterns. For example, a system command, such a createOrder(), that updates multiple services would need to be implemented as a Saga that consists of the following steps (each of which is an ACID transaction):

  1. Order Service - create an Order
  2. Customer Service - update the Customer to reserve credit.
  3. Notification Service - send an email to the Customer.

Sagas can be complicated

On the surface, sagas might seem simple but they require you, as a developer, to deal with various issues that were previously handled by the database. For example, you must implement compensating transactions, which undo previously committed updates. For example, in the Create Order Saga, the credit reservation step could fail due to insufficient credit, and so the first step would need a compensating transaction that rejects the order.

You must also handle lack of automatic isolation - sagas are ACD - by using countermeasures, which are application-level design techniques that implement isolation.

Robust asynchronous messaging is complicated

Moreover, since sagas often use asynchronous messaging for coordination you must also handle several low level issues. You must, for example, implement the Transaction outbox pattern to ensure that an operation atomically updates the database and sends a message. Also, since a message broker can deliver the same message multiple times you need to implement the Idempotent receiver pattern to ensure that message handlers are idempotent.

Lack of ACID is the price to pay for the benefits of microservices

Of course, if you have decided to use microservices for your application, then it’s because the benefits of having independently deployable services outweigh the drawbacks of some operations needing to be eventually consistent. Specifically, you use microservices instead of a monolith because it better resolves one or more of the dark energy forces. For example, improved team autonomy, faster deployment pipeline, etc.

Modular monolith: one transaction per domain?

You could implement operations in a modular monolith using one transaction per domain. A multi-domain operation such as createOrder() would be implemented as a saga with each step being an ACID transaction in a different domain:

  1. Order Management - create an Order
  2. Customer Management - update the Customer to reserve credit.
  3. Notification Management - send an email to the Customer.

The downside, however, is that, as I described above, this is much more complex than one transaction per operation.

Modular monolith: one transaction per operation

As you can see, there are compelling reasons to implement each system command as an ACID transaction. I’d argue that in a modular monolith, an operations that spans multiple domains should be implemented as a single ACID transaction. It’s simply…. err.. simpler. It avoids the complexities and overhead of patterns, such as Saga, Transaction outbox, Idempotent receiver, etc. For example, even if you use the observer pattern (e.g. pub-sub) within your monolith, then the observers should execute with the operation’s transaction.

But transactions spanning domains is not modular?

I suppose that an ACID transaction span multiple domains might violate encapsulation. But on the other hand, I’m not sure what problems that typically creates. Conversely, limiting each transaction to a single domain demonstrably adds complexity without any of the upsides of microservices.

But what about one DDD aggregate per transaction?

Interestingly, there are some in the DDD community that argue that a database should only update a single aggregate. The basis of this argument is the rule that an aggregate is a consistency boundary. While this rule makes sense, I don’t interpret as requiring a database transaction to a single aggregate. An aggregate is simply responsible for maintaining the consistency of its constituent entities and value objects.

When to use eventual consistency

Of course, very few design rules are absolute. You should consider breaking one command = one transaction rule when it creates a tangible problem. But you need to be sure that it’s worth the complexity and overhead of using eventual consistency.

Moreover, I’d try to avoid using compensating transactions. You should structure the Saga so there are no compensatable transactions. Its first step, which might span multiple domains, is the only one that can fail due to business rule violations - it’s the saga’s pivot transaction. The remaining steps are retriable transactions, which cannot violate business rules, and could be triggered via pub-sub.

For example, the Create Order Saga consists of the following steps (each an ACID transaction):

  1. Customer Management and Order Management - create an Order, update the Customer to reserve credit and publish an event (using the Transaction Outbox pattern) that triggers step 2
  2. Notification Management - send an email to the customer.

The first step implements the business rules that could cause the operation to fail. Since it uses the Transaction Outbox pattern, the second second is only trigger if the first step succeeds.

In summary: in a modular monolith avoid eventual consistency unless it solves a problem

Life’s too short. Keep things simple as possible. Solve actual business problems.

What’s next

In this article, I described the trade-offs of using ACID transactions and Sagas to implement system commands that update multiple domains. In a later articles, I’ll discuss other related modular monolith design issues including how to implement queries.

Need help with accelerating software delivery?

I’m available to help your organization improve agility and competitiveness through better software architecture: training workshops, architecture reviews, etc.

Learn more about how I can help


architecting   modular monolith   service collaboration  


Copyright © 2024 Chris Richardson • All rights reserved • Supported by Kong.

About Microservices.io

Microservices.io is brought to you by Chris Richardson. Experienced software architect, author of POJOs in Action, the creator of the original CloudFoundry.com, and the author of Microservices patterns.

New workshop: Architecting for fast, sustainable flow

Enabling DevOps and Team Topologies thru architecture

DevOps and Team topologies are vital for delivering the fast flow of changes that modern businesses need.

But they are insufficient. You also need an application architecture that supports fast, sustainable flow.

Learn more and register for my June 2024 online workshops....

NEED HELP?

I help organizations improve agility and competitiveness through better software architecture.

Learn more about my consulting engagements, and training workshops.

LEARN about microservices

Chris offers numerous other resources for learning the microservice architecture.

Get the book: Microservices Patterns

Read Chris Richardson's book:

Example microservices applications

Want to see an example? Check out Chris Richardson's example applications. See code

Virtual bootcamp: Distributed data patterns in a microservice architecture

My virtual bootcamp, distributed data patterns in a microservice architecture, is now open for enrollment!

It covers the key distributed data management patterns including Saga, API Composition, and CQRS.

It consists of video lectures, code labs, and a weekly ask-me-anything video conference repeated in multiple timezones.

The regular price is $395/person but use coupon ILFJODYS to sign up for $95 (valid until April 12, 2024). There are deeper discounts for buying multiple seats.

Learn more

Learn how to create a service template and microservice chassis

Take a look at my Manning LiveProject that teaches you how to develop a service template and microservice chassis.

Signup for the newsletter


BUILD microservices

Ready to start using the microservice architecture?

Consulting services

Engage Chris to create a microservices adoption roadmap and help you define your microservice architecture,


The Eventuate platform

Use the Eventuate.io platform to tackle distributed data management challenges in your microservices architecture.

Eventuate is Chris's latest startup. It makes it easy to use the Saga pattern to manage transactions and the CQRS pattern to implement queries.


Join the microservices google group