Skip to main content
unbound mongodb at scale

MongoTemplate vs MongoRepository vs Raw Driver

4 min read Chapter 14 of 72

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 @Query annotation), 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 Document objects. 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:

MetricMongoRepositoryRaw Driver + Codec
Dashboard query p5042ms22ms
Dashboard query p99185ms98ms
CPU utilization at 1000 req/s78%52%
Young GC per minute4528
Allocation rate850 MB/sec520 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.