2

In our Java EE application, we have a setup where Service A is an endpoint. In some valid business cases, this endpoint throws an exception and returns an error code to the client — this is expected behavior and cannot be changed.

However, Service A also calls Service B, which is part of another module (included as a Maven dependency). Service B performs an update (clearing activity data), which we want to persist, even when Service A throws the expected error.

The problem: this update is rolled back along with the outer transaction, so the update in Service B is lost.

Here’s a simplified version of the code:

// Service A (has maven dep on ServiceB)
@POST
@Path("/{id}/book")
public Response bookXXXX(..) {
 try {
 serviceB.doSmth(); // should persist even if error follows
 ...
 } catch (SpecificServiceBException e) {
 throw Response.error(Status.CONFLICT, someInfo); // expected and cannot be changed
 }
}
// Service B 
@ApplicationScoped
public class ServiceB {
 @Inject
 private IRepository repository;
 public void deleteActivity(Long key) {
 ActivityEntity activity = find...;
 activity.updateLastDate(null);
 repository.update(activity);
 }
}

We tried using @Transactional(REQUIRES_NEW), but this leads to deadlocks in the database (Oracle). Because the database now has two concurrent transactions and waits for the first ("outer one") to commit and vice versa.


I also tried the recommended TransactionSynchronizationManager from this article to no success.

How can we make Service B’s update persist independently of Service A's transaction, even when Service A throws an exception (as part of normal flow)?

PS: We use Java EE but not Spring.

BalusC
1.1m377 gold badges3.7k silver badges3.6k bronze badges
asked Jun 24, 2025 at 12:24
8
  • A deadlock or a lock? A deadlock means there are 2 rows involved. Java code is less relevant - it's more interesting to hear what data you store and how it's inter-dependent. Commented Jun 24, 2025 at 12:46
  • A deadlock, both transactions are stuck forever. Both transactions write to related tables (at least related via FK...) so obviously this would cause a deadlock. I dont know how to navigate around this. Commented Jun 24, 2025 at 12:55
  • Why the update is rolled back? Your code snippets don't contain any code responsible for it. And what is the version of Jakarta EE you use? Commented Jun 24, 2025 at 13:13
  • It's a usual lock. Deadlocks are detected right away, and you get a respective error right away. In your case it's a Foreign Key lock specifically: linkedin.com/posts/… Commented Jun 24, 2025 at 13:16
  • To be clear: serviceA runs in a transaction context that you do want to roll back when it throws an exception, yes? Commented Jun 24, 2025 at 13:39

3 Answers 3

2

You need to ensure your first transaction doesn't hold a lock necessary for your 2nd transaction.

Option1: Reverse the order of transactions. First your "nested" tx commits and releases the locks, and only then you start your "outer" tx.

Option2: Have a yet another transaction first - the one that modifies the rows that are needed for the "nested" tx. Then introduce the rest of the changes in the 3d transaction.

Option3: remove the FK constraint. Since that's what you're locked on. Or reverse the FK if it's a one-to-one relationship. Note though, if you drop the constraint - YOU will be responsible for enforcing it in your Java code.

answered Jun 24, 2025 at 13:13
Sign up to request clarification or add additional context in comments.

Comments

1

Since Service A runs in a (JTA) transaction context that must be rolled back when A throws an exception, nothing that must avoid rollback when A throws can be associated with A's transaction.

Service B is such a thing, or at least its use in conjunction with Service A is. Therefore, in order for B to execute after execution of A starts and complete before execution of A completes, it must be invoked with semantics equivalent to either @Transactional(REQUIRES_NEW) or @Transactional(NOT_SUPPORTED). The latter is presumably not viable, and anyway, both of these share the issue that service B will be unable to access resources locked by the A context from which it is invoked.

Evidently, B does need to access a resource that the associated A transaction locks. This leaves you only two main alternatives:

  1. Refactor A and / or B and / or the underlying resources such that the two services do not (exclusively) access any of the same resources. Any resource that one modifies would be wholly off limits to the other, which might be a broader restriction than it seems on first sight. Having done this, let A invoke B with @Transactional(REQUIRES_NEW) semantics.

  2. Refactor A so that it doesn't invoke B (from within its own transaction context) at all. Presumably, something else would invoke the two, sequentially. That other something might be non-transactional, or it might invoke B with @Transactional(REQUIRES_NEW) semantics.

If neither of these seems viable then that would be a strong sign that your requirements are inconsistent.

answered Jun 24, 2025 at 14:30

6 Comments

It seems the only possible path is refactoring service B by using @Transactional(REQUIRES_NEW) and reordering calls to the tables. Since @Transactional(REQUIRES_NEW) will cause locks we had to call this section as early as possible (and it has to complete) before other sections of the outer transaction try to access similiar tables.
@JosipDomazet, note well that "reordering calls to the tables" is not among the alternatives that I described. Even if you successfully get past the locking issue that way, there is a good chance that conflicting accesses by two concurrent transactions will cause the one committing second to fail. This depends on transaction isolation level and many other details, so perhaps you could make it work, but even if you did, the result would be brittle. Nevertheless, good luck.
@JohnBollinger, why do you mention JTA? It's about distributed transactions and it was renamed to Jakarta Transactions. But OP doesn't use it - he accesses the same database in both transactions.
@StanislavBashkyrtsev, Java EE transactions, whatever label you want to use for them, can support distributed transactions, but that's far from their only use case. Their distinguishing feature is that they provide application-level transactions, both container-managed and application-managed, and they are routinely part of Java EE applications of all shapes and sizes. Management of database-level transactions is part of this, for as many databases are involved. And although use of Java EE is a pretty strong sign by itself, it was the OP who first raised @Transactional.
@JohnBollinger, you're right. I wonder why I hear of JTA exclusively in the context of distributed transactions. Maybe it's because I use Spring and the question of JTA doesn't arise unless we need a distributed tx..
|
0

Using @Transactional(REQUIRES_NEW) was a natural idea but lead to deadlock because both transactions might try to lock the same table and Oracle doesn’t support true nested transactions.

We solved this by using CDI events with TransactionPhase.AFTER_COMPLETION instead.

The key idea is:

  • Instead of performing the update directly in Service B we fired a CDI event.

  • The event observer listens with @Observes(during = TransactionPhase.AFTER_COMPLETION).

  • This means the observer will be triggered after the outer transaction completes, regardless of whether it commits or rolls back.

  • The observer method is marked @Transactional, so it runs in a new clean transaction, without interfering with the original one.

This avoids the deadlock and allows us to persist the update even though Service A throws an error.

At the time the observer is triggered (AFTER_COMPLETION) the original transaction has fully ended. There's no transaction context left, so we're free to start a new one (via regular @Transactional) without Oracle getting upset about conflicting locks.

Requirements

This pattern assumes:

  • It's running in a full Java EE / Jakarta EE container like WildFly, JBoss, Payara, or GlassFish.

  • CDI is available, and you're using container-managed transactions (@Transactional).

  • If you're on plain Tomcat, you'll need to add CDI support yourself (e.g. using Weld SE) — it doesn’t support this out of the box.

Example flow (simplified)

// Service A
try {
 serviceB.fireClearEvent();
 throw new BusinessException(); // triggers rollback
}
// Service B
event.fire(new ClearActivityEvent(key));
// Observer
@ApplicationScoped 
public class ClearActivityEventObserver {
...
@Transactional
public void handle(@Observes(during = AFTER_COMPLETION) ClearActivityEvent event) {
 // Update the entity in a new transaction
}
answered Jul 1, 2025 at 12:42

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.