Scheduling methods to execute post transaction commit using Spring TransactionSynchronization
The annotation we all needed
The Problem
When using transactions in spring, often it is needed for some specific methods to run post the transaction is successfully committed, let's understand this with an example:-
Assume there are two microservices in a system, first one manages user lifecycle, let's call it User Management Service (UMS) and the other one prepares feed for the User (Feed Service).
When a New User signs up on the platform UMS does some validation and then pushes a message to create_feed
Kafka topic. The feed service on the other hand listens to messages on create_feed
topic and then calls UMS to get the interests of the user to prepare the feed. Do you see a huge problem in this flow!? What if any of the subsequent methods during sign up fail — the user details were never be saved in the system and the Feed service get user interest call
will start failing. This issue gets amplified when are talking about multiple microservice interactions.
There could be many other requirements where it is needed for some specific methods to only execute post transaction commit, lets see how this can be solved elegantly!
The Solution 🚀
To execute a block of code after transaction commit afterCommit
method of TransactionSynchronizationAdapter
can be used, which should be something like this
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// code for publishing message to kafka
}
});
While this works it requires one to add a lot of boilerplate code everywhere one use it and is simply not a very clean way of solving the problem.
Let's look at how to create an annotation (@PostCommit
) and use spring AOP around advice to drive all this from the background. The idea is whenever a method with @PostCommit
annotation on it is encountered the execution is wrapped inside a runnable and added for execution in a ThreadLocal, when the transaction is complete afterCommit
of TransactionSynchronizationAdapter
will be called and all the runables
of ThreadLocal
can be executed.
#1 Building the Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostCommit {
}
This part is straightforward and similar to creating any other annotation in Java
#2 Building the PostCommitAdapter
The PostCommitAdapter will serve two functions
- It will register
runnables
on aThreadLocal
- Override
AfterCommit
ofTransactionSynchronizationAdapter
to run all registeredrunnables
on transaction commit
@Slf4j
@Component
public class PostCommitAdapter extends TransactionSynchronizationAdapter {
private static final ThreadLocal<List<Runnable>> RUNNABLE = new ThreadLocal<>();
// register a new runnable for post commit execution
public void execute(Runnable runnable) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
List<Runnable> runnables = RUNNABLE.get();
if (runnables == null) {
runnables = new ArrayList<>();
RUNNABLE.set(runnables);
TransactionSynchronizationManager.registerSynchronization(this);
}
return;
}
// if transaction synchronisation is not active
runnable.run();
}
@Override
public void afterCommit() {
List<Runnable> runnables = RUNNABLE.get();
runnables.forEach(Runnable::run);
}
@Override
public void afterCompletion(int status) {
RUNNABLE.remove();
}
}
If the transaction is active the execute
method registers runnables
in ThreadLocal
otherwise, it simply goes ahead and executes it. The afterCommit
runs all the runnables
that is inside the ThreadLocal
#3 Connecting the Adapter & Annotation using around advice
To hook the execute method of PostCommitAdapter
with @PostCommit
annotation, an around advice is created on @PostCommit
annotation which at every join point encapsulates the method execution inside the runnable and calls execute
of PostCommitAdapter
@Aspect
@Slf4j
@AllArgsConstructor
@Configuration
public class PostCommitAnnotationAspect {
private final PostCommitAdapter postCommitAdapter;
@Pointcut("@annotation(com...<package>..PostCommit)")
private void postCommitPointCut(){}
@Around("postCommitPointCut()")
public Object aroundAdvice(final ProceedingJoinPoint pjp) {
postCommitAdapter.execute(new PjpAfterCommitRunnable(pjp));
return null;
}
private static final class PjpAfterCommitRunnable implements Runnable {
private final ProceedingJoinPoint pjp;
public PjpAfterCommitRunnable(ProceedingJoinPoint pjp) {
this.pjp = pjp;
}
@Override
public void run() {
try {
pjp.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
}
Usage
Once the boilerplate is written, the usage is as simple as it can get, whichever method is supposed to be executed post transaction commit one has to simply annotate it with PostCommit
annotation.
Example: Consider two classes A & B having PostCommit annotated method
Class A {
@PostCommit
void log(){
log.info("log from class A")
}
}Class B {
@PostCommit
void log(){
log.info("log from class B")
}
}
and a driver class which are calling these methods:
Class mainClass {
@Transactional
void transactionalMethod(Entity entity){
someOperation(entity)
log.info("inside transaction");
a.log();
b.log();
save(entity);
log.info("end of method");
}
}
the expected output will be:
> inside transaction
> ** saving entity
> log from class A
> log from Class B
Conclusion
This blog tries to explain how we can leverage Spring’s TransactionSynchronization
to execute a block of code after a transaction commit and how it can be hooked to a custom annotation using Spring AOP to drive everything from behind.
If you need any more help feel free to connect with me. Do follow for all the new updates. Thanks for the read. Cheers 🍺
Other Java and Spring Articles and Resources you may like