Bank / Wallet
Transactions, Atomic inc/dec, Optimistic Locking
What Morphium Offers
Morphium's @MorphiumTransactional CDI interceptor wraps MongoDB multi-document
transactions in a single annotation. Combined with atomic inc()/dec()
operations and @Version-based optimistic locking, it covers the full spectrum from
simple atomic updates to complex cross-collection transactions.
The Challenge
MongoDB transactions require explicit session management, careful error handling and manual commit/abort logic. Without a framework, transaction boundaries spread across the codebase and are easy to get wrong.
Morphium Features Used
Prerequisites & Key Concepts
@MorphiumTransactionalis a CDI interceptor, not an entity annotation. It goes on the service method (e.g.transfer()), not on the entity class. The Quarkus Morphium extension registers the interceptor automatically.inc()/dec()are server-side atomic operations. They translate to MongoDB's$incoperator and execute without reading the document first. This avoids race conditions on balance updates.@Versionauto-increments on eachstore(). If two threads load the same Account (both see version 3), the first store succeeds (version → 4), and the second fails because its expected version (3) no longer matches. This is optimistic locking — no database locks are held.- Transfer is a denormalized audit log. It stores
fromAccountandtoAccountas Strings (account numbers), not as@Referencelinks. This makes the transfer history self-contained and query-friendly.
Entity Relationship
Account (@Entity, @FieldNameConstants, @Builder)
_id: MorphiumId @Id
accountNumber: String @Index(unique)
ownerName: String
balance: double
currency: String
version: Long @Version
createdAt: LocalDateTime @CreationTime
String Reference →
Transfer (@Entity, @FieldNameConstants, @Builder)
_id: MorphiumId @Id
fromAccount: String
toAccount: String
amount: double
currency: String
description: String
status: String
createdAt: LocalDateTime @CreationTime
@Entity
@Embedded
@Reference
Entity Source Code
Account.java
Java
import de.caluga.morphium.annotations.CreationTime; import de.caluga.morphium.annotations.Entity; import de.caluga.morphium.annotations.Id; import de.caluga.morphium.annotations.Index; import de.caluga.morphium.annotations.Version; import de.caluga.morphium.driver.MorphiumId; import lombok.Data; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.experimental.FieldNameConstants; import java.time.LocalDateTime; @Entity(collectionName = "accounts")1 @Data @NoArgsConstructor @AllArgsConstructor @Builder @FieldNameConstants2 public class Account { @Id3 private MorphiumId id; @Index(options = {"unique:1"})4 private String accountNumber; private String ownerName; private double balance; @Builder.Default private String currency = "EUR"; @Version5 private Long version; @CreationTime6 private LocalDateTime createdAt; }
1 Maps this class to the
accounts MongoDB collection.2 Lombok: generates
Account.Fields.accountNumber, .balance etc. for type-safe queries.3 Primary key, mapped to MongoDB
_id. Auto-generated if null.4 Unique index — MongoDB rejects duplicate account numbers at database level.
5 Optimistic locking — Morphium increments on every
store() and throws VersionMismatchException on conflict.6 Automatically set on first
store(), never updated again.
Transfer.java
Java
import de.caluga.morphium.annotations.CreationTime; import de.caluga.morphium.annotations.Entity; import de.caluga.morphium.annotations.Id; import de.caluga.morphium.driver.MorphiumId; import lombok.Data; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.experimental.FieldNameConstants; import java.time.LocalDateTime; @Entity(collectionName = "transfers")1 @Data @NoArgsConstructor @AllArgsConstructor @Builder @FieldNameConstants public class Transfer { @Id private MorphiumId id; private String fromAccount;2 private String toAccount; private double amount; private String currency; private String description; @CreationTime3 private LocalDateTime createdAt; private String status; }
1 Stored in a separate
transfers collection — acts as an immutable audit log.2 Account references stored as plain Strings (not
@Reference) — the record survives even if the referenced account is deleted.3 Automatically timestamped on creation — ideal for audit records.
Transaction Code
@MorphiumTransactional Transfer
Morphium API — @MorphiumTransactional
Java
import de.caluga.morphium.Morphium; import de.caluga.morphium.quarkus.transaction.MorphiumTransactional; @MorphiumTransactional1 public TransferResult transfer(String fromNumber, String toNumber, double amount, String description) {2 // Atomic decrement on source account var fromQ = morphium.createQueryFor(Account.class) .f(Account.Fields.accountNumber).eq(fromNumber);3 morphium.inc(fromQ, Account.Fields.balance, -amount);4 // Atomic increment on target account var toQ = morphium.createQueryFor(Account.class) .f(Account.Fields.accountNumber).eq(toNumber); morphium.inc(toQ, Account.Fields.balance, amount); // Store audit log inside the same transaction Transfer t = Transfer.builder() .fromAccount(fromNumber).toAccount(toNumber) .amount(amount).description(description) .status("COMPLETED").build(); morphium.store(t);5 return new TransferResult(true, "OK", t);6 }
1 CDI interceptor that wraps the entire method in a MongoDB multi-document transaction; commits on success, aborts on any exception.
2 Returns a
TransferResult record (success flag, message, transfer entity). Accepts four parameters including a human-readable description.3 Fluent query builder:
.f(Account.Fields.accountNumber) uses Lombok-generated @FieldNameConstants for type-safe field references.4
morphium.inc() with a negative value debits the source balance atomically via MongoDB's $inc operator — no read required.5 Stores the audit-log
Transfer document inside the same transaction so the record is only visible after a successful commit.6 Returns a typed result record instead of void, allowing the caller to inspect the outcome and the created
Transfer entity.Service Code (Deposit / Withdraw)
Morphium API — Atomic inc/dec
Java
import de.caluga.morphium.Morphium; import de.caluga.morphium.query.Query; @Inject Morphium morphium; // Deposit — atomic server-side increment public void deposit(String accountNumber, double amount) { Query<Account> q = morphium.createQueryFor(Account.class) .f("accountNumber").eq(accountNumber); morphium.inc(q, "balance", amount);1 } // Withdraw — atomic decrement (inc with negative) public void withdraw(String accountNumber, double amount) { Query<Account> q = morphium.createQueryFor(Account.class) .f("accountNumber").eq(accountNumber); morphium.inc(q, "balance", -amount);2 } // Create account — store() with @Version for optimistic locking public void createAccount(Account account) { morphium.store(account); // @Version auto-set to 13 }
1
morphium.inc() translates to a MongoDB $inc update — the balance is incremented server-side without reading the document first.2 Passing a negative value to
inc() performs a decrement. Morphium also provides dec() convenience methods (they delegate to inc() with a negated amount), so either approach works.3 On first
store() Morphium initializes the @Version field to 1; every subsequent store increments it for optimistic-locking checks.Related Documentation
- Optimistic Locking — @Version, Conflict Detection
- Developer Guide — Transactions, @MorphiumTransactional
- API Reference — inc(), dec(), Atomic Operations