skip to content

When would you use ReactiveMongoTemplate instead of a ReactiveMongoRepository?

level: middleimportance: should knowfreq 50%

answer

  1. Repository = declarative CRUD
  2. Template = imperative, dynamic Criteria/Query
  3. Template does aggregation, partial update, upsert
  4. Same MappingMongoConverter under both
  5. Custom fragment mixes them cleanly

basics

~10 s

Use the repository for simple CRUD and derived queries. Drop to ReactiveMongoTemplate when you need dynamic queries, aggregations, partial updates, upserts, or fine control the repository abstraction cannot express, still returning Mono/Flux.

solid answer

~40 s

ReactiveMongoRepository is the high-level, declarative option: interface methods, derived queries, and @Query. ReactiveMongoTemplate is the lower-level, imperative API that both are built on. You reach for the template when you need things the repository cannot express cleanly: programmatically built Criteria/Query objects, aggregation pipelines (aggregate returning Flux), field-level partial updates and upserts via update()/findAndModify(), bulk operations, collation/hints, or tailable cursors through the tailable option. Both surfaces return Mono/Flux and share the same MappingMongoConverter, so documents map identically. A common pattern is to keep repositories for the 80% simple CRUD and inject ReactiveMongoTemplate into a custom repository fragment for the complex 20%, keeping call sites uniform.

code

java · 22 lines
java
@Repository
class UserRepositoryImpl implements UserRepositoryCustom {

    private final ReactiveMongoTemplate template;
    UserRepositoryImpl(ReactiveMongoTemplate template) { this.template = template; }

    // dynamic query the derived API can't build cleanly
    public Flux<User> search(SearchCriteria c) {
        Query q = new Query();
        if (c.status() != null) q.addCriteria(Criteria.where("status").is(c.status()));
        if (c.minAge() != null) q.addCriteria(Criteria.where("age").gte(c.minAge()));
        return template.find(q.with(Sort.by("createdAt").descending()), User.class);
    }

    // partial, atomic field update — no whole-document save
    public Mono<Void> markSeen(String id) {
        return template.updateFirst(
                Query.query(Criteria.where("_id").is(id)),
                new Update().set("lastSeen", Instant.now()).inc("visits", 1),
                User.class).then();
    }
}

go deeper

for a junior

Know both exist and that the repository is the simpler default.

for a middle

Explain concrete cases the template covers (aggregation, partial update, dynamic Criteria) and the custom-fragment pattern.

for a senior

Reason about lost-update risk of save vs partial update and driver options only the template exposes.

for a principal

Set team conventions for when to escalate to the template and how to keep mapping/consistency guarantees.

Spring Data Mongo gives you **two reactive surfaces** over the same driver and the same object mapping: **1. `ReactiveMongoRepository<T, ID>`** — declarative. You write an interface; Spring Data derives queries from method names (`findByStatusAndCreatedAfter`) or from `@Query("{ ... }")` JSON. Great for standard CRUD and fixed finders. Limited when the query shape is dynamic or the operation is not a plain find/save/delete. **2. `ReactiveMongoTemplate`** — imperative. It is the workhorse both the repository proxies and you can call directly. Key methods (all returning `Mono`/`Flux`): - `find(Query, Class)` / `findOne` — build a `Query` from `Criteria.where(...).is(...)` fluently, add `.with(Sort)`, `.limit()`, `.skip()`, `.collation()`. - `aggregate(Aggregation, ...)` → `Flux` — run aggregation pipelines (`match`, `group`, `project`, `lookup`). - `updateFirst` / `updateMulti` / `upsert` with an `Update` object — **partial** field updates (`Update.update("field", v).inc("count", 1)`) without loading the document. Repositories only offer whole-document `save`. - `findAndModify` / `findAndReplace` — atomic read-modify-write. - `insert` vs `save` — insert-only vs upsert-by-id semantics. - tailable cursors via `Query.query(...).with(...)` and the **`tailable()`** cursor option, or the higher-level `@Tailable` on a repository method. **Why they coexist.** Both use the same `MappingMongoConverter`/`MongoMappingContext`, so `@Document`, `@Id`, `@Field` map identically. Repositories are literally implemented **on top of** the template, so there is no consistency risk in mixing them. **Custom fragment pattern (idiomatic).** Keep the repository interface for common calls, but add a custom fragment for the complex bits: ``` interface UserRepository extends ReactiveMongoRepository<User,String>, UserRepositoryCustom {} interface UserRepositoryCustom { Flux<User> search(SearchCriteria c); } class UserRepositoryImpl implements UserRepositoryCustom { /* inject ReactiveMongoTemplate */ } ``` Call sites just use `UserRepository`; internally the fragment uses the template. **Gotchas.** (1) A whole-document `save` from a repository overwrites concurrent changes to *other* fields; prefer a template partial `update` for field-scoped writes to avoid lost updates. (2) Aggregations and dynamic criteria are not expressible via derived method names — do not fight the abstraction, use the template. (3) Both are non-blocking; still never `.block()` on the event loop. **When to use which.** Repository first for readability; template when you need dynamic query construction, aggregations, partial/atomic updates, bulk writes, tailable cursors, or driver options (hint, collation, read preference) the derived API cannot express.

  • How do you add a custom method backed by ReactiveMongoTemplate to a repository?
    Define a UserRepositoryCustom interface, implement it in UserRepositoryImpl (naming convention: interface name + Impl) with the template injected, and make the main repository extend both ReactiveMongoRepository and the custom interface.
  • Why prefer template updateFirst over repository save for a single-field change?
    save rewrites the entire document, which can clobber concurrent updates to other fields (lost update). updateFirst with an Update object mutates only the targeted fields atomically on the server.

saying these in an interview costs you the question

  • Thinking repository and template use different mappers, risking inconsistent documents
  • Claiming derived query methods can express aggregation pipelines
  • Using whole-document save for atomic single-field increments

context