How do you build an aggregation pipeline with match, group, and project stages in Spring Data MongoDB?
answer
- newAggregation(match, group, project, sort)
- match FIRST → uses index, cuts data
- group key → _id; accumulators .sum().as()
- aggregate(agg, In.class, Out.class).getMappedResults()
- $match before group = WHERE, after = HAVING
basics
~10 sUse Aggregation.newAggregation(...) with static stage helpers: match(Criteria) filters, group(fields).sum/count aggregates, project(...) reshapes output. Run it with mongoTemplate.aggregate(agg, "collection", OutputType.class), then read results via getMappedResults().
solid answer
~40 sThe Aggregation framework runs a multi-stage server-side pipeline where each stage's output feeds the next. In Spring you compose it with org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation(stage1, stage2, ...). Key stages via static imports: match(Criteria) → $match to filter early (put it first, before grouping, to use indexes and cut data); group("field") → $group with accumulators like .sum("amount").as("total"), .count().as("n"), .avg(...), .max(...), .addToSet(...); project(...) → $project to include/exclude fields, rename, and compute expressions. group's _id becomes the grouping key, exposed as "_id" unless you project it to a friendly name. Execute with mongoTemplate.aggregate(agg, Order.class, OrderStats.class) — first class = input collection, second = the mapped output type. Read results from AggregationResults.getMappedResults(). Ordering matters: match→group→sort→limit is the canonical, index-friendly order.
code
java · 21 linesimport static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import org.springframework.data.domain.Sort;
Aggregation agg = newAggregation(
match(where("status").is("PAID")), // $match: filter early
group("customerId") // $group by customer
.sum("amount").as("total")
.count().as("orders"),
project("total", "orders") // $project reshape
.and("_id").as("customerId")
.andExclude("_id"),
sort(Sort.by(Sort.Direction.DESC, "total")),
limit(10)
);
AggregationResults<CustomerRevenue> results =
mongoTemplate.aggregate(agg, Order.class, CustomerRevenue.class);
List<CustomerRevenue> topCustomers = results.getMappedResults();go deeper
Recognizes aggregate as a pipeline of stages and can name match/group/project.
Builds a working match→group→project pipeline and reads getMappedResults into a DTO.
Explains stage ordering for index use, group _id semantics, and the raw-$ vs typed-helper distinction.
Weighs pipeline performance (index-eligible $match, allowDiskUse, 16MB/100MB limits), DTO mapping design, and when aggregation beats application-side computation.
**Aggregation** is MongoDB's data-processing pipeline: documents flow through an ordered list of **stages**, each transforming the stream, similar to SQL's GROUP BY/HAVING/SELECT but composable. Spring Data models this with the `org.springframework.data.mongodb.core.aggregation` package. **Building a pipeline.** `Aggregation.newAggregation(TypedClass, stage1, stage2, ...)` (or the untyped overload) creates an `Aggregation`. Stages come from static factory methods on `Aggregation`: - **`match(Criteria)`** → `$match`. Filters documents, exactly like a find query. **Put it as early as possible** — a `$match` before `$group` can use indexes and drastically reduces the documents flowing downstream. - **`group(fields...)`** → `$group`. Buckets documents by a key and computes accumulators. The grouping key is stored in the output field `_id`. Accumulators: `.sum("amount").as("total")`, `.count().as("orders")`, `.avg("price").as("avgPrice")`, `.max/.min`, `.first/.last`, `.addToSet("tag").as("tags")`, `.push(...)`. Group with no field (`group()`) collapses everything into one bucket — good for grand totals. - **`project(...)`** → `$project`. Reshapes each document: `.and("_id").as("customerId")` renames, `.andInclude("total")`, `.andExclude("_id")`, and computed expressions via `.andExpression("total * 0.9")` (SpEL-like) or `ArithmeticOperators`, `StringOperators`, etc. - Other common stages: **`sort(Sort)`** → `$sort`, **`limit(n)`/`skip(n)`**, **`unwind(field)`** → `$unwind`, **`lookup(...)`** → `$lookup`, **`count().as(...)`**, **`bucket(...)`**, **`facet(...)`**. **Executing.** `mongoTemplate.aggregate(aggregation, inputType, OutputType.class)` or `aggregate(aggregation, "collectionName", OutputType.class)`. The **input** determines the source collection; the **output type** is what each result document maps to (often a dedicated DTO). You get an `AggregationResults<OutputType>`; call `.getMappedResults()` for the list, or `.getUniqueMappedResult()` for a single-row expectation. **Field references and the `$` gotcha.** Inside group/project, referencing an existing field's value in raw Mongo requires a `$` prefix (`"$amount"`), but Spring's typed helpers (`.sum("amount")`) add it for you. Mixing raw `$field` strings with typed helpers is a frequent bug — know which API you're in. **Grouping-key access.** After `group("customerId")`, the key lives under `_id`. To surface it with a clean name, add a `project().and("_id").as("customerId")` (and usually `.andExclude("_id")`) so your DTO field maps cleanly. **Edge cases / gotchas.** - **Stage order is semantic AND performance-critical**: `$match` before `$group` filters early; `$match` after `$group` filters aggregated results (the equivalent of SQL HAVING). Putting `$sort` before `$limit` gives top-N; after, it sorts a truncated set. - **16 MB result limit** for a single returned document; large pipelines may need `allowDiskUse` (see `AggregationOptions`) for big `$group`/`$sort` that exceed the 100 MB in-memory stage limit. - **Type mapping**: computed fields not present on the output DTO are dropped during mapping — make sure DTO field names match the projected names. - Empty pipeline results return an empty list, not null. **When to use.** Aggregation is for analytics/reporting/reshaping that go beyond a simple find: totals per group, joins via `$lookup`, flattening arrays via `$unwind`, computed/derived fields. For simple filtered reads, a plain Query is lighter.
- Why should $match come before $group rather than after?A $match before $group can use collection indexes and shrinks the document stream, so grouping does less work. A $match after $group filters on aggregated results (like SQL HAVING) and cannot use the source indexes — semantically different and usually slower.
- After group("customerId"), how do you expose the key as a named field in your DTO?The key is stored under _id. Add a project stage: .and("_id").as("customerId").andExclude("_id"), so the output maps to a DTO field named customerId instead of _id.
saying these in an interview costs you the question
- Placing $match after $group when they mean to filter source rows
- Thinking group's key is a normal field name rather than _id
- Manually prefixing $ on fields inside typed helpers like .sum("$amount")
- Expecting getMappedResults() to return null (it returns empty list)
- Assuming unlimited result size — ignoring the 16 MB doc / 100 MB stage limits