Audit Log
Capped Collections, Lifecycle Callbacks, Automatic Logging
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
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 -fon a log file. Only capped collections support tailable cursors. - Automatic creation — Morphium creates the capped collection on the first
store()call ifautoIndexAndCappedCreationOnWriteis 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
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; }
@Entity(collectionName=...) maps this class to a MongoDB collection with an explicit name.@Capped instructs Morphium to create a fixed-size FIFO collection; maxSize is in bytes, maxEntries caps the document count.@FieldNameConstants generates compile-time constants like AuditEntry.Fields.action for type-safe query field references.@CreationTime is automatically set by Morphium on the first store() and never overwritten, making it a reliable audit timestamp.Service Code
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
@Builder provides a fluent builder; the timestamp field is left unset because @CreationTime will populate it.morphium.store() creates the capped collection automatically on the first write if it does not already exist.// 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
sort(Map.of(field, -1)) sorts by timestamp descending so the newest entries appear first.limit(N) restricts how many documents MongoDB returns, keeping memory usage bounded.countAll() runs a server-side count query without transferring documents to the application.Related Documentation
- Developer Guide — @Capped Collections, @Lifecycle
- API Reference — store(), sort(), limit(), countAll()