Optimistic Locking with @Version

Morphium supports optimistic concurrency control via the @Version annotation. It prevents lost updates — a situation where two concurrent writes overwrite each other silently — without requiring database-level locks.

How it works

When a field is annotated with @Version, Morphium manages it automatically:

EventBehaviour
First store() (INSERT)Field is set to 1L
Every subsequent store() (UPDATE)Field is incremented by 1L
Concurrent write detectedVersionMismatchException is thrown

The version value is checked server-side on every UPDATE. If the document in the database already has a higher version number, the update is rejected and the caller must reload and retry.

Basic usage

import de.caluga.morphium.annotations.Entity;
import de.caluga.morphium.annotations.Id;
import de.caluga.morphium.annotations.Version;
import de.caluga.morphium.driver.MorphiumId;

@Entity
public class Order {
    @Id
    private MorphiumId id;

    @Version
    private long version;       // managed by Morphium — do not set manually

    private String status;
    private double total;

    // getters / setters
}
// First store — version is set to 1 automatically
Order order = new Order();
order.setStatus("PENDING");
order.setTotal(99.90);
morphium.store(order);  // order.getVersion() == 1 after this call

// Second store — version is incremented to 2
order.setStatus("CONFIRMED");
morphium.store(order);  // order.getVersion() == 2 after this call

Custom field name

By default the MongoDB field name follows the camelCase convention (same rule as @Property). You can override it:

@Version(fieldName = "v")   // stored as "v" in MongoDB
private long version;

Handling conflicts

When a stale entity is stored, Morphium throws VersionMismatchException:

import de.caluga.morphium.VersionMismatchException;

try {
    morphium.store(order);
} catch (VersionMismatchException e) {
    long staleVersion = e.getExpectedVersion(); // the version we tried to write with
    // reload from DB and apply changes again
    Order fresh = morphium.createQueryFor(Order.class)
        .f("_id").eq(order.getId())
        .get();
    fresh.setStatus(order.getStatus());
    morphium.store(fresh); // fresh version from DB → succeeds
}

Inheritance

@Version can be placed on a base class. All subclasses inherit the optimistic-locking behaviour:

@Entity
public abstract class BaseDocument {
    @Id private MorphiumId id;

    @Version
    private long version;
}

@Entity(collectionName = "orders")
public class Order extends BaseDocument {
    private String status;
}

@Entity(collectionName = "invoices")
public class Invoice extends BaseDocument {
    private double amount;
}

Entities without @Version

Entities without a @Version field are not affected. Morphium falls back to the standard replace behaviour for those classes.

Testing with InMemoryDriver

@Version is fully supported by the InMemoryDriver, so no real MongoDB is needed for tests:

MorphiumConfig cfg = new MorphiumConfig();
cfg.setDriverName("InMemDriver");
cfg.connectionSettings().setDatabase("test");

try (Morphium morphium = new Morphium(cfg)) {
    Order o = new Order();
    morphium.store(o);
    assert o.getVersion() == 1L;

    Order copy = morphium.createQueryFor(Order.class).f("_id").eq(o.getId()).get();
    copy.setStatus("CONFIRMED");
    morphium.store(copy);   // copy.version → 2

    o.setStatus("CANCELLED");
    assertThrows(VersionMismatchException.class, () -> morphium.store(o)); // o still has version=1
}

Comparison with JPA @Version

JPA @VersionMorphium @Version
Annotationjavax.persistence.Versionde.caluga.morphium.annotations.Version
ExceptionOptimisticLockExceptionVersionMismatchException
Version init0 (first persist sets to 1)1L on first store
Typeint, long, Timestamp, …long / Long
BackendSQL WHERE id=? AND version=?MongoDB {$and:[{_id},{version}]} filter
In-memory testingRequires JPA provider setupInMemoryDriver — zero infrastructure
← Back to Documentation Hub