Feature-rich MongoDB ODM and messaging framework for Java 21+
Available languages: English and Deutsch
- ποΈ High-performance object mapping with annotation-driven configuration
- π¨ Integrated message queue backed by MongoDB (no extra infrastructure)
- β‘ Multi-level caching with cluster-wide invalidation
- π Custom MongoDB wire-protocol driver tuned for Morphium
- π§ͺ In-memory driver for fast tests (no MongoDB required)
- π± PoppyDB β MongoDB-compatible in-memory server: replica sets, auth/TLS, messaging backend
- π― JMS API (experimental) for standards-based messaging
- π Java 21+ β modern language baseline (pattern matching, sealed types)
Morphium is the only Java ODM that ships a message queue living inside MongoDB. If you already run MongoDB, you can power persistence, messaging, caching, and change streams with a single component.
| Feature | Morphium | Morphium + PoppyDB | Spring Data + RabbitMQ | Kafka |
|---|---|---|---|---|
| Infrastructure | MongoDB only | None β embedded Java server | MongoDB + RabbitMQ | MongoDB + Kafka |
| Setup complexity | β Very low | β Minimal (one dependency) | βββ Medium | βββββ High |
| Message persistence | Built in | Snapshots (optional) | Optional | Built in |
| Message priority | β Yes | β Yes | β Yes | β No |
| Distributed locks | β Yes | β Yes | β No | β No |
| Throughput, one-way sendβreceive* | ~870β1,250 msg/s | ~770β4,900 msg/s | 10Kβ50K msg/s | 100K+ msg/s |
| Round-trip requestβresponse (ping-pong)* | 89 msg/s | 223 msg/s (2.5Γ) | β | β |
| Operations | β Very easy | β Trivial (single process) | ββ Medium | ββββ Complex |
* All numbers are indicative and depend heavily on hardware and workload; Morphium's are
measured, the RabbitMQ/Kafka columns quote typical vendor/
community figures. The two rows measure different things. One-way counts sendβreceipt
only (no processing, no reply): ~870β1,250 msg/s against a 3-node MongoDB replica set
(depending on the client host); PoppyDB
runs in-process and therefore scales with the host β ~770 msg/s on a small 4-core CI host,
~2,100 msg/s on an M1 Max laptop, ~4,300β4,900 msg/s on an M1 Ultra desktop β in-process,
it simply scales with the host. Round-trip measures complete ping-pongs (request out,
response received): 223 msg/s at 4.5 ms latency against PoppyDB vs. 89 msg/s at 11.3 ms
against the MongoDB replica set β 2.5Γ the throughput at less than half the latency, thanks
to PoppyDB and Morphium Messaging being optimized for each other (both sides detect the
counterpart). Re-measured 2026-08-07 with the Morpheus load generator (100 msg/s fixed
rate, 5 sender threads, Mac Studio client): median round-trip 2.4 ms against a local
PoppyDB replica set vs 5.7 ms against the MongoDB replica set β note that this run was
not like-for-like (PoppyDB local, MongoDB over the network), so part of that gap is
network, not broker. A symmetric re-measurement on 2026-08-11 β client inside the
homelab network, both backends separate processes on dedicated hosts at equal distance β
confirms the ratio at 2.34β2.49Γ: MongoDB p50 4.97/5.12 ms vs PoppyDB p50 2.13/2.06 ms
over two runs (3001 pings each, zero loss). The tail is where they really diverge: MongoDB
p99 42β129 ms at 100 msg/s on an idle cluster, PoppyDB below 7 ms, with 2.5β3Γ lower jitter.
A same-session A/B attributes 8β18 % lower median RTT to the 2026-08 messaging
optimizations (answers dispatched before the processed_by write, non-exclusive messages
processed straight from the change-stream fullDocument). PoppyDB's strength is latency,
not raw one-way throughput on constrained hardware. Persistence there is snapshot-based, see the
PoppyDB section below.
How real is Kafka's 100K+ figure β and how big is the gap really? We measured both on
one and the same laptop-class machine (Apple M1 Max, single-node Kafka 4.1, ~200-byte
payload, one consumer, end-to-end from first send to last receipt β the same setup as our
one-way benchmark).
In its normal operating mode β asynchronous sends, client-side batching β Kafka reached
~900K msg/s, so the 100K+ column is real and even conservative on modern hardware. But
forced into Morphium's semantics, where every message is sent synchronously and individually
acknowledged by the broker (4 sender threads, acks=all), Kafka drops to ~8β10K msg/s vs.
~1,800 msg/s for Morphium+PoppyDB on the same machine β a factor of 4β5, not 100+. Kafka's
headline throughput comes almost entirely from batching thousands of records into each
network round-trip (with no per-message broker ack and, by default, no per-message fsync β
durability comes from replication), not from faster per-message handling. Morphium Messaging
deliberately sends each message as an individually acknowledged insert; the remaining 4β5Γ
is the price of a full ODM insert (object mapping, wire protocol, change-stream dispatch)
per message.
Where exactly does Morphium's per-message cost go? Decomposed on the same machine: a
raw morphium.insert of the very same Msg document into PoppyDB runs at ~4,600 docs/s β
0.33 ms per operation single-threaded, on par with Kafka's ~0.5 ms per-request latency, so
the wire protocol and server are not the problem. An active change-stream watcher brings
that to ~3,600 docs/s (fanout, ~20 %), and the full messaging layer (topic registry,
listener dispatch, processing queue) lands at ~2,500β2,800 msg/s once the JVM is warm β the
~1,800 msg/s above is a cold-start figure. The 2026-08 optimization round (duplicate-_id
insert pre-check is an O(1) index lookup instead of an O(N) collection scan, one dead
messaging index removed, non-exclusive messages processed straight from the change-stream
fullDocument with no per-message re-read) additionally made insert cost independent of
collection size β the former O(N) _id scan degraded to double-digit inserts/s on a
200K-document collection, the index lookup holds >200K inserts/s there (A/B-measured on an
M1 Ultra); its effect on the M1-Max figures in this paragraph has not been re-measured yet.
The real limiting
factor is write concurrency: PoppyDB's in-memory backend serializes writes per collection,
so raw throughput plateaus at ~4,600
inserts/s no matter how many sender threads you add (1 thread: ~3,100/s; 2+: ~4,300β4,600/s).
Per-message-acknowledged throughput on par with Kafka's synchronous mode (~8β10K msg/s) is
the realistic ceiling for future server-side concurrency work β not 100K+, which no system
reaches without batching.
Does client-side batching help Morphium the way it helps Kafka? Yes, when it's a genuine
single-round-trip bulk insert β no annotation, no tuning: against MongoDB it roughly
quadruples end-to-end throughput over unbatched sendMessage() (1128 vs. 291 msg/s, chunks of
100). The @WriteBuffer annotation, tried as the "just annotate it" shortcut, turned out to
be the wrong tool β it predates Morphium Messaging and flushes on a polling housekeeping
thread, which becomes a throughput ceiling, not a booster, once producers outrun the poll
interval. Full numbers, and two real bugs this probe surfaced along the way (both already
fixed on develop), in the "Batch Send Throughput"
section.
PoppyDB is Morphium's sibling product: an in-memory server that speaks the MongoDB wire
protocol. Any client connects β mongosh, Compass, PyMongo, the official drivers, and of
course Morphium. It starts in milliseconds and needs zero infrastructure: no Docker, no
Testcontainers, no MongoDB installation.
- Wire protocol, change streams, aggregation pipeline, indexes, transactions
- Replica-set emulation with real leader election and automatic failover
- SCRAM authentication + TLS (6.3.0) β
mongoshlogs in exactly as against real MongoDB - Declarative user provisioning (6.3.0) via
--users-fileβ idempotent, replicated, version-gated - Snapshot persistence β periodic dumps, automatic restore on startup
- Messaging backend β server-side optimizations specifically for Morphium Messaging
<dependency>
<groupId>de.caluga</groupId>
<artifactId>poppydb</artifactId>
<version>6.3.6</version>
<scope>test</scope>
</dependency>PoppyDB server = new PoppyDB(27017, "localhost", 100, 10);
server.start();
// ... any MongoDB client can connect to localhost:27017 now ...
server.shutdown();The embedded route above is Java-only; the CLI jar works for every stack. It is a single
self-contained jar from Maven Central (classifier cli) β your Python/Node/Go/Rust
integration tests get a MongoDB-compatible server in milliseconds, no Docker image, no
Testcontainers, nothing to install:
curl -O https://repo1.maven.org/maven2/de/caluga/poppydb/6.3.6/poppydb-6.3.6-cli.jar
# start for a test run: --no-config keeps it isolated from any stray
# ~/.config/poppydb/config on a developer machine - same flags, same behavior in CI
java -jar poppydb-6.3.6-cli.jar --port 27017 --no-configPoint your test suite at mongodb://localhost:27017, kill the process afterwards β state is
gone (unless you want persistence, see below). --help lists all options.
The CLI is not just a test tool, though: as a messaging backend it is production-ready β that is exactly what PoppyDB's server-side messaging optimizations are for. Run it with snapshot persistence, a replica set for HA, and auth/TLS (all below), and you have a standing message broker with a single jar. It is a general-purpose MongoDB replacement only for dev/test β but for Morphium Messaging it is the recommended dedicated backend, see the deployment playbook.
java -jar poppydb-6.3.6-cli.jar --port 27017 --dump-dir ./data --dump-interval 300Snapshots every 5 minutes, final dump on shutdown, automatic restore on the next start.
Config can also live in a properties file: --cfg /etc/poppydb/config (validate it upfront
with --check-config, inspect the effective result with --print-config).
One process per node, each with the same seed list β election picks the primary, failover is automatic:
java -jar poppydb-6.3.6-cli.jar -p 17017 --rs-name myrs \
--rs-seed host1:17017,host2:17017,host3:17017 --rs-priorities 100,50,50Users (admin.system.users) replicate across the set, so logins survive failover.
java -jar poppydb-cli.jar -p 27018 --auth --rootUser admin --rootPassword s3cr3t \
--ssl --sslKeystore server.jks --sslKeystorePassword changeit
mongosh "mongodb://admin:s3cr3t@localhost:27018/test?authSource=admin"For provisioning a whole user set declaratively, point --users-file at a JSON file β applied
idempotently on every leadership change, protected against rollback by a version gate.
Morphium Messaging runs on PoppyDB as its backend β a full message queue (topics, exclusive delivery, request/response) with a single Java dependency. This is a production use case, not a test trick: PoppyDB and Morphium Messaging are optimized for each other, and a standalone PoppyDB (CLI, with persistence + replica set + auth/TLS) makes a dedicated message broker without operating a MongoDB:
PoppyDB server = new PoppyDB(27017, "localhost", 100, 10);
server.start();
try (Morphium morphium = new Morphium(cfg)) { // cfg points at localhost:27017
MorphiumMessaging messaging = morphium.createMessaging();
messaging.addListenerForTopic("orders", (mq, msg) -> {
System.out.println("new order: " + msg.getValue());
return null;
});
messaging.start();
}π Deep dives: Online documentation Β· PoppyDB guide Β· Production deployment playbook Β· Migrating from MongoDB
- Documentation hub β entry point for all guides
- Overview β core concepts, quick start, compatibility
- Upgrade v6.2βv6.3 β what changes in 6.3.x
- Upgrade v6.1βv6.2 β migration checklist for 6.2.x
- Migration v5βv6 β step-by-step upgrade guide
- InMemory Driver Guide β capabilities, caveats, testing tips
- PoppyDB Guide β the MongoDB-compatible in-memory server in depth
- PoppyDB Deployment Playbook β config file, replica sets, auth/TLS in production
- Optimistic Locking (
@Version) β prevent lost updates with@Version - SSL/TLS & MONGODB-X509 β encrypted connections and certificate-based authentication
- Aggregation examples:
docs/howtos/aggregation-examples.md - Messaging implementations:
docs/howtos/messaging-implementations.md - Performance guide:
docs/performance-scalability-guide.md - Production deployment:
docs/production-deployment-guide.md - Monitoring & troubleshooting:
docs/monitoring-metrics-guide.md
Four patch releases within one week is not our usual cadence, so here is what happened. We ran a deliberate deep-code-review campaign over the change-stream and replication code β several AI reviewers (different vendors, reviewing independently) plus verification of every finding against the code. That review surfaced a whole class of bugs that only exist under load: silent event loss on change-stream resume, live events overtaking history replay, unbounded memory pinning, a sync gate opening over a dead watch. None of them had ever been reported by a user β which is exactly what makes them dangerous: this kind of bug does not file an issue, it shows up months later as quietly diverged data.
- 6.3.4 shipped those fixes β and one of them carried a client-side follow-up bug (#329, the resume-token loop described below). Found in production within hours.
- 6.3.5 fixed that loop the same day. 6.3.4 is marked defective.
- 6.3.6 fixes a topology-erosion bug in the connection pool's failover handling (#330) that a rolling server restart exposed on our staging cluster: one client in thirty ended up silently bus-dead while its HTTP side looked perfectly healthy.
If you are on any 6.3.x: upgrade straight to 6.3.6, in the order described below. The remaining review findings are architectural and scheduled for 6.4.0 β this series is the end of the storm, not a new normal. The full story of each fix is in the CHANGELOG; the short version is: we would rather ship four honest patch releases in a week than sit on known silent-data-loss bugs.
β οΈ Upgrade order matters: clients first, then servers β and skip 6.3.4.6.3.4 added strict server-side resume-window validation for change streams. Correct β but every client up to and including 6.3.4 carries a resume-token bug (#329): when the server ends a stream with
ChangeStreamHistoryLost(code 286), the monitor discards its resume token, then immediately resurrects it and retries β forever, with no backoff. A PoppyDB restart resets the token sequence space, so the moment a 6.3.4+ server comes back up, every connected pre-6.3.5 client enters this loop at once and effectively DDoSes the server (~3.3k errors/s per node observed live) until each client process is restarted by hand. On real MongoDB the same loop starts whenever a consumer's resume point falls off the oplog β rarer, same hammering. 6.3.4 is marked defective; upgrade straight to 6.3.6 (see the release-storm note above).The safe rollout order is therefore the reverse of the usual instinct: 1. upgrade all client applications to β₯ 6.3.5 (they handle history-lost with a single discard and a fresh watch), 2. only then deploy/restart the PoppyDB servers. A server deployed first arms the loop in every client that has not been upgraded yet.
From 6.3.5 on, a PoppyDB server with a dump directory also persists its change-stream sequence (
sequence-state.properties) across restarts, so orderly restarts no longer invalidate resume tokens at all. The very first restart after upgrading still resets the space once (the old server never wrote the state file) β with β₯ 6.3.5 clients that costs one warning line per stream and nothing else.
morphium-jakarta-data implements Jakarta Data 1.0 on top of Morphium's query engine β @Repository interfaces with query derivation from method names, JDQL via @Query (including GROUP BY/HAVING compiled into an aggregation pipeline), offset and cursor/keyset pagination. quarkus-morphium builds on it for CDI integration: config mapping, @MorphiumTransactional, health checks, Dev Services, Dev UI, GraalVM native-image support, and build-time repository generation via Gizmo. Both are optional β core has no dependency on either, and -DskipExtensions still produces a core-only build. See Jakarta Data and Quarkus Extension.
Note: the Quarkus extension moved from io.quarkiverse.morphium:quarkus-morphium:1.2.0 to de.caluga:quarkus-morphium:6.3.0. Coordinates only β no package renames, no API changes.
A third messaging implementation: the standard single collection and cursor for broadcast/topic traffic, plus a dedicated per-recipient collection with its own cursor and dispatcher thread for directed messages and answers. Select it with cfg.messagingSettings().setMessagingImplementation("DualChannelMessaging"). Beta on purpose β past saturation it trades a little throughput for markedly better tail latency. See docs/howtos/messaging-implementations.md.
β οΈ All messaging participants on a queue must run the same implementation. This has always been true forSingleCollectionMessagingandMultiCollectionMessaging, and it applies toDualChannelMessagingtoo: the implementations use different collection layouts and there is no bridge between them. A mismatch fails silently β a Standard node waiting for an answer from a Dual Channel responder times out forever, because the answer goes into the requester's DM collection, which Standard never reads. Switch every node together, and drain or pause request/reply traffic while you do.Since 6.3.1 a mismatch is detected: every instance announces its implementation in a layout-independent
<queue>_participantscollection and checks the other participants on startup β WARN by default;cfg.messagingSettings().setMessagingImplementationCheck(ImplementationCheck.THROW)makes a mismatched instance refuse to start instead (#280).
One database roundtrip less per non-exclusive message (processed straight from the change-stream fullDocument), event-driven delivery of requeued messages, configurable default TTL and fallback-poll cadence, change-stream liveness driving the fallback poll, and a processing decision trace for diagnosing answer timeouts.
Real SCRAM-SHA-1/SCRAM-SHA-256 authentication with opt-in enforcement (--auth), declarative user provisioning from a file (--users-file) and users that replicate across the replica set instead of living on one node. Configuration files (--cfg, --print-config, --check-config) keep secrets off the command line, --log-level stops the DEBUG firehose, and a DevOps command surface adds live currentOp/killOp, rs.conf(), listCommands, hostInfo, dbHash and a validate that really walks the indexes.
Two heap watermarks (--memory-warn / --memory-reject, decided on the post-GC live set) reject document-creating writes with a retryable ExceededMemoryLimit before the heap dies, while updates, deletes and TTL expiry stay allowed so the system can drain. The 16MB BSON document limit is now enforced like mongod instead of merely advertised, and maxMessageSizeBytes is respected end-to-end with byte-aware write-batch splitting.
New aggregation stages ($merge, $documents, $densify, $fill, $setWindowFields, $collStats, $listSessions, and a real $out), ~40 additional expression operators, positional update operators $/$[]/$[<identifier>] with arrayFilters, and $bit. Plus a long list of correctness fixes β among them $geoWithin with $center/$centerSphere/$polygon, which matched every document, UTC-correct date operators with a 1-based $month, and $project inclusion mode actually restricting output.
PoppyDB replication is now lossless, order-preserving and covers index definitions. Fixed: a re-syncing secondary broadcasting its initial-sync wipe as change-stream drop events (which could destroy admin.system.users cluster-wide during a stepdown), a demoted leader stuck at primary == true, rs.status() reporting a dead peer as SECONDARY forever, and a plaintext internal election/replication channel that made --auth/--ssl ineffective on a replica set. On the client side, the failover read path could throw a raw NPE past every retry.
Insert's duplicate-_id pre-check is an O(1) index lookup instead of a full scan under the write lock, the change-stream before-image is no longer deep-copied twice per watched update, and the index-store rebuild ping-pong between an open transaction and concurrent readers is gone.
Upgrading is covered step by step in the migration guide; see CHANGELOG for full details.
Morphium is now a multi-module project: morphium-parent (BOM), morphium (core library), and poppydb (server). The core library de.caluga:morphium no longer drags in server dependencies (Netty, etc.) β 90% leaner for users who just need the ODM.
The former MorphiumServer became an independent module de.caluga:poppydb in 6.2 β see the
PoppyDB section above for what it does and
how to use it.
MorphiumDriverException extends RuntimeException β consistent with the MongoDB Java driver. Eliminates 40+ boilerplate catch-wrap-rethrow blocks.
@Reference now supports cascadeDelete and cascadeStore for automatic lifecycle management of referenced entities.
Annotation-driven auto-increment sequences β no manual counter management needed.
Works correctly with store() and storeList(), supports @CreationTime on Date, long, and String fields.
Morphium detects Azure CosmosDB connections and automatically adjusts behavior for compatibility.
The 6.2.x patch releases brought continuous improvements, among them: server-side recipient filtering and a liveness watchdog for messaging, a defaultQueryTimeoutMS setting, field-name translation in Aggregator and Query.distinct(), a dedicated MorphiumDocumentTooLargeException, and numerous PoppyDB/InMemoryDriver robustness fixes. The later patches (6.2.5β6.2.10) focused on production hardening of the wire path and messaging: mid-message read timeouts no longer desynchronize the wire stream, replies are verified against their request id (responseTo), change streams resume from the last token across restarts instead of silently skipping events, and exclusive messages can no longer be processed twice when their lock is lost mid-processing.
See CHANGELOG for full details.
MorphiumDriverException extends RuntimeException instead of Exception. This eliminates boilerplate catch-wrap-rethrow blocks but requires attention in existing code:
// Multi-catch β simplify (MorphiumDriverException IS a RuntimeException now)
// Before:
catch (RuntimeException | MorphiumDriverException e) { ... }
// After:
catch (RuntimeException e) { ... }
// throws declarations β can be removed (but still compile if left in)
// Before:
public void doStuff() throws MorphiumDriverException { ... }
// After:
public void doStuff() { ... }
// Standalone catch β works unchanged
catch (MorphiumDriverException e) { ... } // still compilesThe embedded MongoDB-compatible server was extracted to its own module and renamed:
| 6.1.x | 6.2.x | |
|---|---|---|
| Maven artifact | included in morphium |
separate: de.caluga:poppydb:6.3.6 |
| Package | de.caluga.morphium.server |
de.caluga.poppydb |
| Main class | MorphiumServer |
PoppyDB |
| CLI JAR | morphium-*-server-cli.jar |
poppydb-*-cli.jar |
| Test tag | @Tag("morphiumserver") |
@Tag("poppydb") |
If you use PoppyDB in tests, add the dependency:
<dependency>
<groupId>de.caluga</groupId>
<artifactId>poppydb</artifactId>
<version>6.3.6</version>
<scope>test</scope>
</dependency>Wire-protocol compatibility is preserved β PoppyDB responds to both poppyDB and morphiumServer in the hello handshake.
MorphiumConfig now organizes settings into typed sub-objects. The old setters still work but are @Deprecated:
// 6.1.x style (deprecated but functional)
cfg.setDatabase("mydb");
cfg.addHostToSeed("localhost", 27017);
// 6.2.x style (preferred)
cfg.connectionSettings().setDatabase("mydb");
cfg.clusterSettings().addHostToSeed("localhost", 27017);
cfg.driverSettings().setDriverName("PooledDriver");Available sub-objects: connectionSettings(), clusterSettings(), driverSettings(), messagingSettings(), cacheSettings(), authSettings(), threadPoolSettings(), objectMappingSettings(), writerSettings().
The morphium core artifact no longer bundles server dependencies (Netty, etc.). If you only use Morphium as ODM, your dependency tree is ~90% leaner β no changes to your pom needed.
- Search for
catch (RuntimeException | MorphiumDriverExceptionβ simplify tocatch (RuntimeException - Search for
import de.caluga.morphium.serverβ replace withimport de.caluga.poppydb - Search for
MorphiumServerβ rename toPoppyDB - Search for
@Tag("morphiumserver")β rename to@Tag("poppydb") - Add
poppydbdependency if you use the embedded server in tests - Optional: migrate direct config setters to sub-object style
- Optional: adopt new features (
@Reference(cascadeDelete),@AutoSequence,@Version)
- Connect to MongoDB instances that require mutual TLS / x.509 client certificates
- Configure via
AuthSettings.setAuthMechanism("MONGODB-X509")together with the existingSslHelpermTLS setup
Prevents lost updates in concurrent environments without requiring pessimistic database locks. See docs/howtos/optimistic-locking.md for the full guide.
- Pattern matching across driver and mapping layers
- Records: Not yet supported as
@Entityor@Embeddedtypes (see #116) - Sealed class support for cleaner domain models
- Virtual threads were introduced in this era but rolled back again in 6.2.x: JDK 21's
synchronizedpinning caused deadlocks under load. Morphium runs on platform threads throughout; virtual threads will be re-evaluated once JEP 491 (JDK 24+) is the baseline.
- SSL/TLS Support: Secure connections to MongoDB instances (added in v6.0)
- Fewer duplicates thanks to refined message processing
- Higher throughput confirmed in internal benchmarking
- Distributed locking for coordinated multi-instance deployments
- No MongoDB required for unit tests or CI pipelines
- Significantly faster test cycles in pure in-memory mode
- ~93% MongoDB feature coverage including advanced operations
- Full aggregation pipeline with
$lookup,$graphLookup,$bucket,$mergeObjects - MapReduce support with JavaScript engine integration
- Array operators including
$pop,$push,$pull,$addToSet - Change streams & transactions available for integration testing
- Drop-in replacement for most development and testing scenarios
- Complete rewrite of the guide set
- Practical examples and end-to-end use cases
- Dedicated migration playbook from 5.x to 6.x
- Architecture insights and best practices
- Java 21 or newer
- MongoDB 5.0+ for production deployments
- Maven
Maven dependencies:
<dependency>
<groupId>de.caluga</groupId>
<artifactId>morphium</artifactId>
<version>[6.2.0,)</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>bson</artifactId>
<version>4.7.1</version>
</dependency>Migrating from v5? β docs/howtos/migration-v5-to-v6.md
<dependency>
<groupId>de.caluga</groupId>
<artifactId>morphium</artifactId>
<version>6.3.6</version>
</dependency>import de.caluga.morphium.Morphium;
import de.caluga.morphium.MorphiumConfig;
import de.caluga.morphium.annotations.*;
import de.caluga.morphium.driver.MorphiumId;
import java.time.LocalDateTime;
import java.util.List;
// Entity definition
@Entity
public class User {
@Id
private MorphiumId id;
private String name;
private String email;
private LocalDateTime createdAt;
// getters/setters
}
// Configuration
MorphiumConfig cfg = new MorphiumConfig();
cfg.connectionSettings().setDatabase("myapp");
cfg.clusterSettings().addHostToSeed("localhost", 27017);
cfg.driverSettings().setDriverName("PooledDriver");
Morphium morphium = new Morphium(cfg);
// Store entity
User user = new User();
user.setName("John Doe");
user.setEmail("john@example.com");
user.setCreatedAt(LocalDateTime.now());
morphium.store(user);
// Query
List<User> users = morphium.createQueryFor(User.class)
.f("email").matches(".*@example.com")
.sort("createdAt")
.asList();import de.caluga.morphium.messaging.MorphiumMessaging;
import de.caluga.morphium.messaging.Msg;
// Messaging setup
MorphiumMessaging messaging = morphium.createMessaging();
messaging.setSenderId("my-app");
messaging.start();
// Send a message
Msg message = new Msg("orderQueue", "Process Order", "Order #12345");
message.setPriority(5);
message.setTtl(300000); // 5 minutes
messaging.sendMessage(message);
// Receive messages
messaging.addListenerForTopic("orderQueue", (m, msg) -> {
// process order ...
return null; // no reply
});# Environment variables
export MONGODB_URI='mongodb://user:pass@localhost:27017/app?replicaSet=rs0'
export MORPHIUM_DRIVER=inmem
# System properties
mvn -Dmorphium.uri='mongodb://localhost/mydb' test
# Properties file (morphium.properties)
morphium.hosts=mongo1.example.com:27017,mongo2.example.com:27017
morphium.database=myapp
morphium.replicaSet=myReplicaSet# All tests
mvn test
# Full build with checks
mvn clean verify
# Tagged test selection
mvn test -Dgroups="core,messaging"
# Run against a real MongoDB instance
mvn test -Dmorphium.driver=pooled -Dmorphium.uri=mongodb://localhost/testdb# Default: in-memory driver (fast, no MongoDB required)
./runtests.sh
# Run tagged suites
./runtests.sh --tags core,messaging
# Parallel runs
./runtests.sh --parallel 8 --tags core
# Retry only failed methods
./runtests.sh --rerunfailed
./runtests.sh --rerunfailed --retry 3
# Single test class
./runtests.sh CacheTests
# Statistics
./runtests.sh --stats
./getFailedTests.sh # list failed methodsRun ./runtests.sh --help to see every option.
Tests are parameterized to run against multiple drivers. Use --driver to select:
# InMemory only (fastest, default)
./runtests.sh --driver inmem
# Against external MongoDB with all drivers (pooled + single + inmem)
./runtests.sh --uri mongodb://mongo1,mongo2/testdb --driver all
# Against external MongoDB with pooled driver only
./runtests.sh --uri mongodb://mongo1,mongo2/testdb --driver pooled
# Against PoppyDB (auto-starts local server)
./runtests.sh --poppydb --driver pooled # --morphium-server is a deprecated aliasComplete test coverage requires running against all backends:
# 1. Fast in-memory tests
./runtests.sh --driver inmem
# 2. Real MongoDB tests
./runtests.sh --uri mongodb://your-mongodb/testdb --driver all
# 3. PoppyDB tests
./runtests.sh --poppydb --driver pooled # --morphium-server is a deprecated aliasNew in v6.1
- β
Unified test base: All tests now use
MultiDriverTestBasewith parameterized drivers - β
Driver selection: Each test declares which drivers it supports via
@MethodSource - β Parallel safe: Tests isolated per parallel slot with unique databases
New in v6.0
- β
Method-level reruns:
--rerunfailedonly re-executes failing methods - β No more hangs: known deadlocks resolved
- β Faster iteration: noticeably quicker partial retries
- β Better filtering: class-name filters now reliable
Run ./runtests.sh --help to see every option.
TestConfig consolidates all test settings. Priority order:
- System properties (
-Dmorphium.*) - Environment variables (
MORPHIUM_*,MONGODB_URI) src/test/resources/morphium-test.properties- Defaults (localhost:27017)
The in-memory driver provides a largely MongoDB-compatible data store fully in memory:
Features
- β Full CRUD operations
- β Rich query operator coverage
- β
Aggregation stages such as
$match,$group,$project - β Single-instance transactions
- β Basic change streams
- β
JavaScript
$wheresupport
Performance
- Significantly faster than external MongoDB for tests
- No network latency
- No disk I/O
- Ideal for CI/CD pipelines
Usage
# All tests with the in-memory driver
./runtests.sh --driver inmem
# Specific tests
mvn test -Dmorphium.driver=inmem -Dtest="CacheTests"See docs/howtos/inmemory-driver.md for feature coverage and limitations.
PoppyDB (formerly MorphiumServer) runs the Morphium wire-protocol driver in a separate process, allowing it to act as a lightweight, in-memory MongoDB replacement.
Maven dependency (server module):
<dependency>
<groupId>de.caluga</groupId>
<artifactId>poppydb</artifactId>
<version>6.3.6</version>
</dependency>Building the Server
mvn clean package -pl poppydb -am -Dmaven.test.skip=trueThis creates poppydb/target/poppydb-6.3.6-cli.jar.
Running the Server
# Start the server on the default port (17017)
java -jar poppydb/target/poppydb-6.3.6-cli.jar
# Start on a different port
java -jar poppydb/target/poppydb-6.3.6-cli.jar --port 8080
# Start with persistence (snapshots)
java -jar poppydb/target/poppydb-6.3.6-cli.jar --dump-dir ./data --dump-interval 300Replica Set Support (Experimental)
PoppyDB supports basic replica set emulation. Start multiple instances with the same replica set name and seed list:
java -jar poppydb/target/poppydb-6.3.6-cli.jar --rs-name my-rs --rs-seed host1:17017,host2:17018Use cases
- Local development without installing MongoDB
- CI environments
- Embedded database for desktop applications
- Smoke-testing MongoDB tooling (mongosh, Compass, mongodump, ...)
Current limitations
- No sharding support
- Some advanced aggregation operators and joins still missing
See docs/poppydb.md for more details on persistence and replica sets.
Organizations run Morphium in production for:
- E-commerce: order processing with guaranteed delivery
- Financial services: coordinating transactions across microservices
- Healthcare: patient-data workflows with strict compliance
- IoT platforms: device state synchronization and command distribution
- Content management: document workflows and event notifications
- Blog: https://caluga.de
- GitHub: sboesebeck/morphium
- Issues: Report bugs or request features on GitHub
Check out the Quarkus Morphium Showcase by Heiko Kopp (Bardioc1977) β a live, interactive demo of Morphium with Quarkus covering CRUD, caching, aggregation pipelines, geospatial queries, messaging, transactions, Jakarta Data, and more. A great way to explore what Morphium can do before writing a single line of code.
We appreciate pull requests! Areas where help is especially welcome:
- InMemoryDriver: expanding MongoDB feature coverage
- Documentation: tutorials, examples, translations
- Performance: profiling and benchmarks
- Tests: broader scenarios and regression coverage
How to contribute
- Fork the repository
- Create a feature branch from
develop(git checkout -b feature/AmazingFeature develop) - Commit your changes (
git commit -m 'Add AmazingFeature') - Push the branch (
git push origin feature/AmazingFeature) - Open a pull request against
develop(notmaster)
Important: master is only updated during releases. All PRs must target develop.
Tips
- Respect test tags (
@Tag("inmemory"),@Tag("poppydb")) - Run
./runtests.sh --tags corebefore submitting - Update documentation when you change APIs
Apache License 2.0 β see LICENSE for details.
Thanks to every contributor who helped ship the Morphium 6.2.x releases and to the MongoDB community for continuous feedback.
A special thank-you goes to Heiko Kopp (Bardioc1977) for countless contributions, real-world feedback from large-scale production deployments, and the excellent Quarkus Morphium Showcase.
Questions? Open an issue on GitHub or browse the documentation.
Planning an upgrade? Follow the migration guide.
Enjoy Morphium! π
Stephan BΓΆsebeck & the Morphium team