Across derived queries, @Query, @Aggregation, and MongoTemplate — how do you choose the right query mechanism, and what are the tradeoffs?
answer
- ladder: derived -> @Query -> @Aggregation -> MongoTemplate
- static vs dynamic is the key axis
- indexes + explain() decide perf, not the API
- fragment Impl for custom MongoTemplate logic
- bind params always; Page counts, Slice doesn't
basics
~10 sUse derived methods for simple readable finders, @Query for native JSON filters/projections too complex for a name, @Aggregation for grouping and multi-stage pipelines, and MongoTemplate when the query must be built dynamically at runtime.
solid answer
~40 sPick by complexity and dynamism. Derived query methods (findByX) win for simple, self-documenting finders — but the name gets unreadable past a few conditions and every field typo is a bootstrap failure. @Query drops to native MongoDB JSON: precise operators, projections (fields), count/exists/delete flags — declarative but static strings, bind params to avoid injection. @Aggregation runs pipelines ($match/$group/$lookup) mapping each output doc to a DTO — great for rollups and joins, but the stages are fixed at compile time. When stages or filters vary at runtime, or you need type-safe construction and options like allowDiskUse, use MongoTemplate's Criteria/Aggregation builders. Cross-cutting: all should be backed by proper indexes; verify with explain(). Choose the simplest mechanism that expresses the query, and escalate only when the current tier can't express it cleanly.
code
java · 20 lines// Custom fragment for a DYNAMIC query the derived/@Query tiers can't express
interface OrderRepositoryCustom {
List<Order> search(OrderFilter f);
}
class OrderRepositoryCustomImpl implements OrderRepositoryCustom {
private final MongoTemplate template;
OrderRepositoryCustomImpl(MongoTemplate template) { this.template = template; }
public List<Order> search(OrderFilter f) {
Criteria c = new Criteria();
if (f.status() != null) c.and("status").is(f.status());
if (f.minAmount() != null) c.and("amount").gte(f.minAmount());
return template.find(new Query(c), Order.class);
}
}
// Callers see ONE repository combining both
interface OrderRepository
extends MongoRepository<Order, String>, OrderRepositoryCustom { }go deeper
Know the four options exist and derived methods are the simplest.
Match each mechanism to a use case and know @Query/@Aggregation are static strings.
Explain the static-vs-dynamic axis, fragment composition, and projection/paging costs.
Drive decisions from index/explain() analysis, escalation heuristics, injection safety, and when to pre-aggregate vs run live pipelines at scale.
Spring Data MongoDB offers a **ladder of query mechanisms**; the design skill is choosing the lowest rung that expresses the need cleanly. **1. Derived query methods** (`findByLastNameAndAgeGreaterThan`). Pros: zero implementation, self-documenting, compile-time-ish safety (property paths validated at **startup** — a typo fails fast). Cons: names balloon with condition count, can't express `$or` mixes / `$elemMatch` / computed conditions cleanly, and keyword semantics (e.g. `Like`→regex) can surprise. **Use for** simple finders and existence/count/delete-by. **2. `@Query`** — native MongoDB **JSON filter** on the method. Pros: full operator control, **projections** (`fields`), `sort`, `count`/`exists`/`delete` flags, collation. Positional `?0`/SpEL binding keeps it injection-safe. Cons: strings are opaque to the compiler (bad JSON fails at execution), field names aren't refactor-safe, and the filter is **static**. **Use for** complex filters/projections that a method name can't express readably. **3. `@Aggregation`** — a **pipeline** (`pipeline = {stages...}`). Pros: grouping, computed fields, `$lookup` joins, faceting; each output doc maps to a **DTO/projection**; trailing `Sort`/`Pageable` append stages. Cons: pipeline is a **fixed compile-time array** — no conditional stages; output shape must match the return type. **Use for** analytics, rollups, joins, top-N. **4. `MongoTemplate`** — the imperative API with **`Criteria`** and the typed **`Aggregation`** builder (`newAggregation(match(...), group(...))`). Pros: fully **dynamic** — build filters/stages from runtime conditions, type-safe stage construction, access to options (`allowDiskUse`, read preference, bulk ops, `findAndModify`). Cons: more code, less declarative, lives in a repository fragment or service. **Use for** dynamic queries, complex conditional logic, and operations outside the repository abstraction. **Composition:** custom logic integrates via **repository fragments** — declare a custom interface + `Impl` using `MongoTemplate`, and have your `MongoRepository` extend it, so callers see one repository. **Cross-cutting concerns (the principal lens):** - **Indexes decide performance**, not the mechanism — every filter/`$match`/`$near`/`$sort` should be index-backed; validate with `explain()` and watch `COLLSCAN`. - **Injection**: always bind parameters (`?0`/Criteria), never concatenate untrusted input into JSON. - **Projection & payload**: prefer `fields`/DTO projections to cut over-fetching; be aware unselected fields are null on the mapped entity. - **Paging cost**: `Page` runs a count query; `Slice` doesn't. Large aggregations may need `allowDiskUse`. - **Refactor safety**: derived/Criteria track property renames better than raw JSON strings. - **Consistency & scale**: consider read preferences, `$lookup` cost, and whether a materialized/pre-aggregated collection beats an expensive live pipeline. **Decision heuristic:** simplest that works → derived; need native operators/projection → `@Query`; multi-stage transform/rollup → `@Aggregation`; anything **dynamic** or needing engine options → `MongoTemplate`. Escalate one rung only when the current one can't express the query cleanly.
- How do you add fully dynamic query logic while keeping a single repository interface for callers?Use a repository fragment: declare a custom interface (e.g. OrderRepositoryCustom) with an Impl that uses MongoTemplate/Criteria, then have MongoRepository extend both. Spring Data merges the proxy and the fragment so callers see one repository.
- Two implementations produce identical results; how do you decide which query is actually better?Check index usage and cost with explain() — look for COLLSCAN vs IXSCAN, documents examined vs returned, in-memory sorts. The mechanism matters less than whether the filter/sort hits an index; pick the one with the better execution plan.
saying these in an interview costs you the question
- Claiming @Aggregation can add stages conditionally at runtime
- Treating the query API choice as the performance driver instead of indexing
- Reaching for MongoTemplate for simple static finders that a derived method expresses clearly