MongoTemplate vs MongoRepository vs Raw Driver
MongoTemplate vs MongoRepository vs Raw Driver
The Symptom
The telemetry query service processes dashboard requests that aggregate readings from multiple sensors. Each request executes 3-5 queries, each returning 100-500 documents. Under load, JFR profiling shows 18% of CPU time spent in MappingMongoConverter.read() and BeanWrapper.setPropertyValue(). The service is CPU-bound, but the bottleneck is not the database or the network. It is the mapping layer.
The Cause
Spring Data MongoRepository wraps MongoTemplate, which wraps the raw MongoDB Java Sync Driver. Each layer adds overhead:
- MongoRepository: Parses the method name into a query (or uses
@Queryannotation), delegates to MongoTemplate. Method name parsing is cached, but the delegation and result type conversion add 0.5-1.0us per call. - MongoTemplate: Constructs the query, invokes the raw driver, maps results through
MappingMongoConverter. The converter uses generated property accessors, but type inspection and conversion still cost 2-3us per document. - Raw Driver: Executes the wire protocol, decodes BSON to
Documentobjects. When using custom codecs, decodes directly to domain objects, eliminating all intermediate allocation.
The Benchmark
@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 10)
@Fork(1)
@State(Scope.Benchmark)
public class DataAccessPatternBenchmark {
private MongoClient rawClient;
private MongoCollection<Document> rawCollection;
private MongoCollection<TelemetryReading> codecCollection;
private MongoTemplate mongoTemplate;
private TelemetryRepository repository;
@Param({"1", "10", "100", "500"})
private int resultSize;
@Setup
public void setup() {
rawClient = MongoClients.create("mongodb://localhost:27017");
rawCollection = rawClient.getDatabase("telemetry")
.getCollection("readings");
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
CodecRegistries.fromCodecs(new TelemetryReadingCodec()),
MongoClientSettings.getDefaultCodecRegistry()
);
codecCollection = rawClient.getDatabase("telemetry")
.getCollection("readings", TelemetryReading.class)
.withCodecRegistry(codecRegistry);
// Spring context setup (simplified)
ApplicationContext ctx = new AnnotationConfigApplicationContext(MongoConfig.class);
mongoTemplate = ctx.getBean(MongoTemplate.class);
repository = ctx.getBean(TelemetryRepository.class);
}
@Benchmark
public List<TelemetryReading> springRepository() {
return repository.findBySensorIdOrderByTimestampDesc(
"sensor-00001",
PageRequest.of(0, resultSize)
);
}
@Benchmark
public List<TelemetryReading> springTemplate() {
Query query = new Query(Criteria.where("sensorId").is("sensor-00001"))
.with(Sort.by(Sort.Direction.DESC, "timestamp"))
.limit(resultSize);
return mongoTemplate.find(query, TelemetryReading.class);
}
@Benchmark
public List<Document> rawDriverDocument() {
return rawCollection.find(Filters.eq("sensorId", "sensor-00001"))
.sort(Sorts.descending("timestamp"))
.limit(resultSize)
.into(new ArrayList<>());
}
@Benchmark
public List<TelemetryReading> rawDriverCodec() {
return codecCollection.find(Filters.eq("sensorId", "sensor-00001"))
.sort(Sorts.descending("timestamp"))
.limit(resultSize)
.into(new ArrayList<>());
}
}
Results (100 documents):
Benchmark (resultSize) Mode Cnt Score Error Units
DataAccessPatternBenchmark.springRepository 100 thrpt 5 2100.000 ± 80.000 ops/s
DataAccessPatternBenchmark.springTemplate 100 thrpt 5 2500.000 ± 95.000 ops/s
DataAccessPatternBenchmark.rawDriverDocument 100 thrpt 5 3400.000 ± 70.000 ops/s
DataAccessPatternBenchmark.rawDriverCodec 100 thrpt 5 4100.000 ± 65.000 ops/s
DataAccessPatternBenchmark.springRepository 100 avgt 5 475.000 ± 15.000 us/op
DataAccessPatternBenchmark.springTemplate 100 avgt 5 400.000 ± 12.000 us/op
DataAccessPatternBenchmark.rawDriverDocument 100 avgt 5 295.000 ± 10.000 us/op
DataAccessPatternBenchmark.rawDriverCodec 100 avgt 5 244.000 ± 8.000 us/op
The mapping tax is real. MongoRepository is 1.95x slower than raw driver with custom codec. MongoTemplate is 1.64x slower. The gap widens with larger result sets because the per-document mapping cost accumulates linearly.
The Fix
Do not rewrite your entire application to use the raw driver. That would throw away Spring Data’s productivity benefits: auditing, query derivation, pagination, and repository abstraction. Instead, identify the hot queries where mapping overhead matters and bypass Spring Data only for those.
@Repository
public class TelemetryReadingFastRepository {
private final MongoCollection<TelemetryReading> collection;
public TelemetryReadingFastRepository(MongoClient mongoClient) {
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
CodecRegistries.fromCodecs(new TelemetryReadingCodec()),
MongoClientSettings.getDefaultCodecRegistry()
);
this.collection = mongoClient.getDatabase("telemetry")
.getCollection("readings", TelemetryReading.class)
.withCodecRegistry(codecRegistry);
}
public List<TelemetryReading> findRecentBySensorId(String sensorId, int limit) {
return collection.find(Filters.eq("sensorId", sensorId))
.sort(Sorts.descending("timestamp"))
.limit(limit)
.into(new ArrayList<>());
}
}
Use this fast repository for the dashboard query endpoint that processes 1,000 requests per second. Keep MongoRepository for CRUD operations, admin endpoints, and any query that runs fewer than 10 times per second.
The Proof
After switching the dashboard query endpoint to the raw driver with custom codec:
| Metric | MongoRepository | Raw Driver + Codec |
|---|---|---|
| Dashboard query p50 | 42ms | 22ms |
| Dashboard query p99 | 185ms | 98ms |
| CPU utilization at 1000 req/s | 78% | 52% |
| Young GC per minute | 45 | 28 |
| Allocation rate | 850 MB/sec | 520 MB/sec |
The Trade-off
Maintaining two data access layers adds complexity. The TelemetryReadingCodec must be updated whenever the entity changes. If a developer adds a field to the Spring Data entity but forgets the codec, the fast path silently drops the field. Mitigate this by keeping the codec in the same package as the entity and adding an integration test that verifies round-trip serialization for both paths.
The raw driver approach also loses Spring Data’s auditing (@CreatedDate, @LastModifiedBy), optimistic locking (@Version), and lifecycle events. For write operations where these features matter, keep using Spring Data.