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

@MorphiumTransactional CDI interceptor that wraps the annotated method in a MongoDB multi-document transaction. On success the transaction is committed; on exception it is aborted. Import: de.caluga.morphium.quarkus.transaction.MorphiumTransactional (Quarkus extension) MongoDBAtlasCosmosDB @Version Optimistic locking field. Morphium auto-increments this value on every store(). If another thread stored a newer version, the update fails with a version conflict. Import: de.caluga.morphium.annotations.Version MongoDBAtlasCosmosDB @CreationTime Auto-populated with the current timestamp when the document is first stored. Import: de.caluga.morphium.annotations.CreationTime MongoDBAtlasCosmosDB inc / dec (atomic) Server-side atomic increment/decrement. morphium.inc(query, field, amount) modifies the field directly in MongoDB without reading the document. No race conditions, no optimistic locking needed for this single-field update. MongoDBAtlasCosmosDB set (field update) Server-side field update. morphium.set(query, field, value) sets a single field atomically. No full-document read/modify/write cycle required. MongoDBAtlasCosmosDB CDI Transaction Events Quarkus CDI events fired before/after transaction commit or abort, enabling audit logging and side-effect decoupling. MongoDBAtlasCosmosDB

Prerequisites & Key Concepts

  • @MorphiumTransactional is 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 $inc operator and execute without reading the document first. This avoids race conditions on balance updates.
  • @Version auto-increments on each store(). 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 fromAccount and toAccount as Strings (account numbers), not as @Reference links. 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