Skip to content

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:

Event Behaviour
First store() (INSERT) Field is set to 1L
Every subsequent store() (UPDATE) Field is incremented by 1L
Concurrent write detected VersionMismatchException 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 @Version Morphium @Version
Annotation javax.persistence.Version de.caluga.morphium.annotations.Version
Exception OptimisticLockException VersionMismatchException
Version init 0 (first persist sets to 1) 1L on first store
Type int, long, Timestamp, … long / Long
Backend SQL WHERE id=? AND version=? MongoDB {$and:[{_id},{version}]} filter
In-memory testing Requires JPA provider setup InMemoryDriver — zero infrastructure