What Morphium Offers

Capped collections + tailable cursors + lifecycle callbacks = automatic audit trails that stream in realtime. @Capped limits collection size, @PreStore/@PostRemove callbacks capture every change, and tailable cursors deliver new entries as they arrive — no polling required.

The Challenge

Audit logging typically requires a separate event bus or manually inserted log entries in every write path. Capped collections with tailable cursors are powerful but require understanding MongoDB's specific constraints around them.

Morphium Features Used

@Capped Creates the MongoDB collection as a capped (fixed-size, FIFO) collection. Parameters: maxSize (bytes) and maxEntries (document count). Oldest documents are automatically removed when limits are reached. Import: de.caluga.morphium.annotations.Capped MongoDBAtlasCosmosDB Tailable Cursor A special cursor type for capped collections that remains open after exhausting results, streaming new documents as they are inserted — similar to Unix "tail -f". Morphium supports tailable cursors via the query API. MongoDBAtlasCosmosDB @Lifecycle Callbacks Morphium fires lifecycle events during store operations: @PreStore and @PostStore for writes, @PreRemove and @PostRemove for deletes, @PostLoad for reads. These are method-level annotations on the entity class. MongoDBAtlasCosmosDB @CreationTime Automatically populated by Morphium with the current timestamp on first store(). Supports Date, long (epoch millis), and String. Not modified on subsequent updates. Import: de.caluga.morphium.annotations.CreationTime MongoDBAtlasCosmosDB @Entity Maps a Java class to a MongoDB collection. The collectionName attribute sets the collection explicitly. Import: de.caluga.morphium.annotations.Entity MongoDBAtlasCosmosDB sort / limit sort(Map.of(field, -1)) orders results descending. limit(N) restricts the number of documents returned from the server. Both are methods on Morphium's Query object. MongoDBAtlasCosmosDB

Prerequisites & Key Concepts

  • Capped Collections are fixed-size MongoDB collections that maintain insertion order. When the size limit (maxSize) or document count limit (maxEntries) is reached, the oldest documents are automatically removed to make room for new ones — no manual cleanup needed.
  • Cannot delete individual documents from a capped collection. You can only drop the entire collection. Updates that would increase a document's size are also not allowed.
  • Tailable Cursors are a special cursor type that remains open after returning all existing results, waiting for and streaming new documents as they are inserted. This is similar to tail -f on a log file. Only capped collections support tailable cursors.
  • Automatic creation — Morphium creates the capped collection on the first store() call if autoIndexAndCappedCreationOnWrite is enabled (default). You cannot convert an existing non-capped collection to capped; you must drop it first.

About Capped Collections

A capped collection is a special MongoDB collection type with a fixed maximum size. Think of it as a circular buffer: once the collection reaches its size limit (e.g. 10 MB) or its document count limit (e.g. 10,000 entries), MongoDB automatically removes the oldest documents to make room for new ones. This FIFO (First In, First Out) behavior makes capped collections ideal for audit logs, event streams, and rolling window data.

Because capped collections maintain insertion order by design, queries that sort by $natural order are extremely efficient — no index is needed. Additionally, capped collections support tailable cursors, which behave like the Unix tail -f command: the cursor stays open after returning all existing documents and streams new documents as they arrive.

Entity Source Code

AuditEntry.java Java
import de.caluga.morphium.annotations.Capped;
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.experimental.FieldNameConstants;

@Entity(collectionName = "audit_log")1
@Capped(maxSize = 10485760, maxEntries = 10000)2
@Data @Builder @NoArgsConstructor @AllArgsConstructor
@FieldNameConstants3
public class AuditEntry {

    @Id
    private MorphiumId id;

    private String entityType;  // e.g. "Product", "Account"
    private String entityId;    // e.g. "product-42"
    private String action;      // CREATE, UPDATE, DELETE
    private String details;     // human-readable description
    private String user;        // who performed the action

    @CreationTime // auto-set on first store(), immutable thereafter4
    private LocalDateTime timestamp;
}
1 @Entity(collectionName=...) maps this class to a MongoDB collection with an explicit name.
2 @Capped instructs Morphium to create a fixed-size FIFO collection; maxSize is in bytes, maxEntries caps the document count.
3 @FieldNameConstants generates compile-time constants like AuditEntry.Fields.action for type-safe query field references.
4 @CreationTime is automatically set by Morphium on the first store() and never overwritten, making it a reliable audit timestamp.

Service Code

Morphium API — Capped Collection Java
import de.caluga.morphium.Morphium;

@Inject Morphium morphium;

// Store a new audit entry — Morphium handles @CreationTime and @Id
AuditEntry entry = AuditEntry.builder()1
    .entityType("Product")
    .entityId("product-42")
    .action("UPDATE")
    .details("Price changed from 9.99 to 12.99")
    .user("alice")
    .build();
morphium.store(entry); // auto-creates capped collection on first write2
1 Lombok's @Builder provides a fluent builder; the timestamp field is left unset because @CreationTime will populate it.
2 morphium.store() creates the capped collection automatically on the first write if it does not already exist.
Querying with sort + limit Java
// Find the 50 most recent audit entries
morphium.createQueryFor(AuditEntry.class)
    .sort(Map.of(AuditEntry.Fields.timestamp, -1))1
    .limit(50)2
    .asList();

// Count all entries in the capped collection
morphium.createQueryFor(AuditEntry.class).countAll();3
1 sort(Map.of(field, -1)) sorts by timestamp descending so the newest entries appear first.
2 limit(N) restricts how many documents MongoDB returns, keeping memory usage bounded.
3 countAll() runs a server-side count query without transferring documents to the application.

Related Documentation