What Morphium Offers

Morphium manages document lifecycle automatically. @Version prevents concurrent overwrites with optimistic locking. @Reference links documents across collections (eager or lazy). @CreationTime, @LastChange and @LastAccess maintain timestamps without application code. Lifecycle callbacks (@PreStore, @PostLoad) let entities react to persistence events.

The Challenge

Versioning, cross-collection references and automatic timestamps typically require custom interceptors or event listeners with significant boilerplate. Morphium handles all of this declaratively through annotations on the entity class.

Morphium Features Used

@Index Creates a MongoDB index on this field. Without options, creates a standard ascending index. Use @Index(options = {"unique:1"}) for a unique constraint enforced at the database level. Import: de.caluga.morphium.annotations.Index MongoDBAtlasCosmosDB @Version Optimistic locking counter. Morphium auto-increments this field on every store() and rejects concurrent updates with stale version numbers. Import: de.caluga.morphium.annotations.Version MongoDBAtlasCosmosDB @Reference Stores a DBRef (or ID) pointing to another @Entity in a separate collection. Set lazyLoading=true for on-demand loading (proxy), or leave default (eager) to resolve immediately on read. Import: de.caluga.morphium.annotations.Reference MongoDBAtlasCosmosDB @Embedded Stores the object inline as a sub-document (no separate collection). The embedded class must NOT have @Entity. Ideal for comments, addresses, and other value objects. Import: de.caluga.morphium.annotations.Embedded MongoDBAtlasCosmosDB @CreationTime Morphium sets this field automatically the first time the entity is stored. It is never updated again. Import: de.caluga.morphium.annotations.CreationTime MongoDBAtlasCosmosDB @LastChange Morphium updates this field automatically on every store() or set() call. Useful for audit trails and cache invalidation. Import: de.caluga.morphium.annotations.LastChange MongoDBAtlasCosmosDB @LastAccess Morphium updates this field every time the entity is read from the database. Useful for tracking activity and implementing TTL-based eviction. Import: de.caluga.morphium.annotations.LastAccess MongoDBAtlasCosmosDB @Lifecycle Marker annotation on the entity class that enables lifecycle callbacks. Required for @PreStore, @PostStore, @PreRemove, @PostRemove, @PostLoad, @PreUpdate, @PostUpdate to fire. Import: de.caluga.morphium.annotations.lifecycle.Lifecycle MongoDBAtlasCosmosDB @PreStore Method-level callback invoked before Morphium writes the entity to MongoDB. Use it for validation, computed field updates, or last-minute transformations. Requires @Lifecycle on the class. Import: de.caluga.morphium.annotations.lifecycle.PreStore MongoDBAtlasCosmosDB @PostLoad Method-level callback invoked after Morphium reads the entity from MongoDB and maps all fields. Use it to re-hydrate @Transient fields or initialize computed state. Requires @Lifecycle on the class. Import: de.caluga.morphium.annotations.lifecycle.PostLoad MongoDBAtlasCosmosDB @Transient Marks a field as non-persistent — Morphium will not store it in MongoDB. Combine with @PostLoad to recompute the value after every read. Import: de.caluga.morphium.annotations.Transient MongoDBAtlasCosmosDB @FieldNameConstants Lombok annotation (not Morphium!). Generates BlogPost.Fields.title, .author etc. at compile time for type-safe queries. Import: lombok.experimental.FieldNameConstants MongoDBAtlasCosmosDB

Prerequisites & Key Concepts

  • Eager vs Lazy @Reference — By default, @Reference is eager: Morphium resolves the referenced entity immediately when the parent is loaded (extra query). With @Reference(lazyLoading = true), Morphium returns a proxy that fetches the target only when a getter is first called. Lazy references reduce initial load time but may cause unexpected queries later.
  • @Reference cascade options@Reference(cascadeDelete = true) automatically deletes referenced entities when the parent is deleted. @Reference(orphanRemoval = true) deletes entities that are removed from a reference field during an update. With automaticStore = true (the default), referenced entities without an ID are auto-stored when the parent is stored — you do not need to store them manually first.
  • @Version counter — When you store an entity with a @Version field, Morphium adds a filter { version: currentValue } to the update. If another thread already incremented the version, the update matches zero documents and Morphium throws VersionMismatchException. This is optimistic locking — no database-level locks are held.
  • @Lifecycle requirement — The class-level @Lifecycle annotation is a prerequisite for any lifecycle method (@PreStore, @PostLoad, etc.) to fire. Without it, the callbacks are silently ignored.
  • @Transient + @PostLoad pattern — Mark a field @Transient so Morphium skips it during serialization. Then annotate a method with @PostLoad to recompute the transient value after every read. This is useful for summary strings, computed flags, or denormalized counts that should not be stored.
  • Container types — Morphium natively handles List<String> (stored as a JSON array) and Map<String, String> (stored as a sub-document). No special annotation is needed for these standard collection types.

Entity Relationship

BlogPost (@Entity)
_id: MorphiumId @Id
title: String @Index
content: String
author: Author @Reference
reviewer: Author @Reference(lazyLoading = true)
comments: List<Comment> @Embedded
tags: List<String>
metadata: Map<String, String>
published: boolean
version: Long @Version
createdAt: LocalDateTime @CreationTime
lastChanged: LocalDateTime @LastChange
lastAccessed: LocalDateTime @LastAccess
editNote: String @Transient
@Reference →
Author (@Entity)
_id: MorphiumId @Id
username: String @Index(unique)
displayName: String
email: String
bio: String
@Embedded →
Comment (@Embedded)
text: String
author: String
createdAt: LocalDateTime
@Entity @Embedded @Reference

Entity Source Code

BlogPost.java Java
import de.caluga.morphium.annotations.*;
import de.caluga.morphium.annotations.lifecycle.Lifecycle;
import de.caluga.morphium.annotations.lifecycle.PreStore;
import de.caluga.morphium.annotations.lifecycle.PostLoad;
import de.caluga.morphium.driver.MorphiumId;
import lombok.Data;
import lombok.experimental.FieldNameConstants;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Entity(collectionName = "blog_posts")1
@Lifecycle2
@Data
@FieldNameConstants
public class BlogPost {

    @Id
    private MorphiumId id;

    @Index3
    private String title;
    private String content;

    @Reference4
    private Author author; // eager by default

    @Reference(lazyLoading = true)5
    private Author reviewer; // loaded on first access

    private List<Comment> comments; // @Embedded sub-documents

    private List<String> tags;

    private Map<String, String> metadata;

    private boolean published;

    @Version6
    private Long version;

    @CreationTime7
    private LocalDateTime createdAt;

    @LastChange8
    private LocalDateTime lastChanged;

    @LastAccess
    private LocalDateTime lastAccessed;

    @Transient9
    private transient String editNote; // not persisted

    @PreStore10
    public void onPreStore() {
        if (tags == null) tags = new ArrayList<>();
        if (metadata == null) metadata = new HashMap<>();
    }

    @PostLoad11
    public void onPostLoad() {
        // Could perform validation or enrichment after loading
    }
}
1 Maps this class to the blog_posts MongoDB collection.
2 Required for lifecycle callbacks (@PreStore, @PostLoad, etc.) to fire. Imported from de.caluga.morphium.annotations.lifecycle.
3 Creates a single-field ascending index on title for fast lookups.
4 Stores a DBRef to the Author in a separate collection; resolved eagerly by default.
5 Lazy reference — Morphium creates a proxy; the Author is not loaded from the database until a method on reviewer is actually called.
6 Optimistic locking counter — Morphium auto-increments on every store and throws VersionMismatchException on conflict.
7 Set once by Morphium when the entity is first persisted; never updated again.
8 Updated automatically by Morphium on every store() or field-level set() call.
9 Not stored in MongoDB. The transient keyword adds Java serialization safety on top of Morphium's @Transient.
10 Called before each write — ensures tags and metadata are never null in the database.
11 Called after each read — can be used for validation, enrichment, or rehydrating transient state.
Author.java Java
import de.caluga.morphium.annotations.Entity;
import de.caluga.morphium.annotations.Id;
import de.caluga.morphium.annotations.Index;
import de.caluga.morphium.driver.MorphiumId;
import lombok.Data;

@Entity(collectionName = "authors")1
@Data
public class Author {

    @Id2
    private MorphiumId id;

    @Index(options = {"unique:1"})3
    private String username;
    private String displayName;
    private String email;
    private String bio;
}
1 Declares this class as a top-level Morphium entity stored in the authors collection.
2 Marks the field as the MongoDB document identifier (_id); MorphiumId wraps an ObjectId.
3 Unique index — MongoDB enforces that no two authors can share the same username, even under concurrent inserts.
Comment.java Java
import de.caluga.morphium.annotations.Embedded;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

@Embedded1
@Data @NoArgsConstructor @AllArgsConstructor
public class Comment {
    private String author;
    private String text;
    private LocalDateTime createdAt;
}
1 Marks this class as an embedded sub-document — stored inline inside the parent document, not in its own collection.

Service Code

Store & Version Bump

Morphium API — Store & Version Java
import de.caluga.morphium.Morphium;
import de.caluga.morphium.query.Query;

@Inject Morphium morphium;1

// Store a new blog post — @Version starts at 1
morphium.store(blogPost);2

// Update the post — @Version auto-increments (1 → 2)
// If another thread stored version 2 first, this throws
// VersionMismatchException (optimistic locking)
blogPost.setTitle("Updated Title");
morphium.store(blogPost);3
1 CDI-injected Morphium instance provided by the Quarkus extension.
2 Persists the entity for the first time; Morphium sets the @Version field to 1.
3 Increments @Version to 2; throws VersionMismatchException if another thread already wrote version 2.

Add Embedded Comment

Add Embedded Comment Java
// Add a comment as an embedded sub-document
Comment comment = new Comment();
comment.setText("Great post!");
comment.setAuthor("Alice");
comment.setCreatedAt(LocalDateTime.now());

// Push directly into the embedded list
morphium.push(blogPost, BlogPost.Fields.comments, comment, false);1

// Or add to list and re-store the whole entity
blogPost.getComments().add(comment);
morphium.store(blogPost); // @Version increments2
1 Appends a single element to the embedded array in MongoDB using an atomic $push update. The fourth parameter (false) disables upsert — the document must already exist.
2 Re-stores the entire entity including the updated comments list; @Version is incremented atomically.
@Reference Eager Loading Java
// When loading a BlogPost, the @Reference author
// is resolved eagerly (extra query to "authors" collection)
BlogPost post = morphium.createQueryFor(BlogPost.class)1
    .f(BlogPost.Fields.title).eq("My First Post")2
    .get();

// author is already populated — no lazy proxy
String authorName = post.getAuthor().getDisplayName();3

// With automaticStore=true (default), Morphium auto-stores the Author
Author author = new Author();
author.setUsername("janedoe");
author.setDisplayName("Jane Doe");
morphium.store(author);4

BlogPost newPost = new BlogPost();
newPost.setAuthor(author); // sets the @Reference5
morphium.store(newPost);
1 Creates a type-safe, fluent query builder for the BlogPost collection.
2 BlogPost.Fields.title is a Lombok-generated compile-time constant — no magic strings.
3 Eager @Reference means the Author is already fully populated when this line runs — no further query is triggered.
4 Explicit store for clarity. With automaticStore = true (the default), Morphium auto-stores referenced entities that lack an ID when the parent is stored.
5 Morphium serializes this assignment as a DBRef (or just the ObjectId) inside the BlogPost document.

Related Documentation