Definition
- Shell queries – the mongo shell provides
findwith a JSON filter document for simple lookups andaggregatewith a pipeline of stages for grouped or computed results. - Aggregation framework – a server-side pipeline that chains stages like
$match,$group,$sort, and$lookupto filter, join, and compute without pulling raw data to the client. - Spring Data derived methods – declaring a method like
findByAccountIdAndAmountGreaterThanon a repository interface lets Spring parse the name and build the query automatically. @Queryannotation – accepts a raw JSON filter with positional parameters (?0,?1) when the method name would get too long.- Custom repository with
MongoTemplate– for aggregation or any query the method name cannot express, you implement a custom interface withMongoTemplateand Spring autowires it into the main repository.
Think of the National Bank of India's transaction records sitting in a big filing cabinet. The mongo shell is you walking up and saying "give me every transaction for Rajesh's account SAV2001 over fifty thousand rupees." The aggregation framework is you handing the clerk a step-by-step checklist – first filter to Rajesh, then group by month, then total the amounts, then sort – and the clerk does all steps and hands back only the final summary. Spring Data is a trained assistant where you just name what you want, like findByAccountIdAndAmountGreaterThan, and the assistant writes the filing-cabinet query for you.
Understanding the Concepts
What is a custom query in MongoDB?
A custom query in MongoDB is any query you write yourself rather than relying on a framework's automatic generation. MongoDB stores documents as BSON (binary JSON), and you query them by passing a filter document that describes the conditions. At the National Bank of India, Rajesh stores transaction records in MongoDB because each channel produces a slightly different document shape, and custom queries let him retrieve exactly the records the business needs.
// Shell: find all transactions for account SAV2001 with amount > 50000
db.transactions.find(
{ accountId: "SAV2001", amount: { $gt: 50000 } }
);
{ "_id": ObjectId("..."), "accountId": "SAV2001", "amount": 75000, "channel": "UPI" }
{ "_id": ObjectId("..."), "accountId": "SAV2001", "amount": 120000, "channel": "NEFT" }
Why does MongoDB have multiple query layers?
MongoDB provides multiple layers because different use cases have different complexity. A simple filter with find is enough to fetch documents matching a condition. But when you need to group, aggregate, join, or reshape data on the server, you need the aggregation framework. And when you build a Java application with Spring, you want type-safe, maintainable code rather than raw shell commands.
findhandles simple CRUD lookups.aggregatehandles multi-stage server-side computation.- Spring Data provides a Java-friendly abstraction over both.
// Shell: project only amount and channel, exclude _id
db.transactions.find(
{ accountId: "SAV2001" },
{ amount: 1, channel: 1, _id: 0 }
);
{ "amount": 75000, "channel": "UPI" }
{ "amount": 120000, "channel": "NEFT" }
How does the aggregation framework work internally?
The aggregation framework accepts an array of stages, and each stage transforms the stream of documents and feeds the output to the next stage. The pipeline executes entirely on the MongoDB server, so only the final result travels to the client. Pankaj designed the National Bank of India's monthly statement pipeline using this approach to avoid pulling millions of raw transactions into the application.
Common stages include:
$match– filters documents (like a WHERE clause).$group– groups documents by a key and applies accumulators like$sum,$avg,$max.$sort– orders the output.$lookup– performs a left outer join to another collection.$project– reshapes each document, including or excluding fields.$unwind– flattens an array field into separate documents.
// Aggregation: monthly spending totals for account SAV2001
db.transactions.aggregate([
{ $match: { accountId: "SAV2001" } },
{ $group: {
_id: { $month: "$txnDate" },
totalSpent: { $sum: "$amount" },
txnCount: { $sum: 1 }
}},
{ $sort: { totalSpent: -1 } }
]);
{ "_id": 3, "totalSpent": 450000, "txnCount": 12 }
{ "_id": 1, "totalSpent": 320000, "txnCount": 8 }
{ "_id": 2, "totalSpent": 210000, "txnCount": 6 }
How do you use Spring Data derived query methods?
In Spring Data MongoDB, you declare a method on a repository interface and Spring parses the method name into a query. The naming convention follows a pattern: findBy + field name + condition keyword. This works for straightforward filters and saves you from writing any query manually.
package com.bank.repository;
import com.bank.model.Transaction;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.math.BigDecimal;
import java.util.List;
public interface TransactionRepository extends MongoRepository<Transaction, String> {
// Spring parses: accountId = ? AND amount > ?
List<Transaction> findByAccountIdAndAmountGreaterThan(
String accountId, BigDecimal amount
);
// Spring parses: channel = ? ORDER BY txnDate DESC
List<Transaction> findByChannelOrderByTxnDateDesc(String channel);
// Spring parses: accountId = ? AND status = ?
long countByAccountIdAndStatus(String accountId, String status);
}
How do you use the @Query annotation?
When the method name gets too long or cannot express the condition, you use @Query with a raw JSON filter. Positional parameters ?0, ?1 map to the method arguments in order. This gives you full MongoDB query syntax while keeping the repository interface clean.
package com.bank.repository;
import com.bank.model.Transaction;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.math.BigDecimal;
import java.util.List;
public interface TransactionRepository extends MongoRepository<Transaction, String> {
@Query("{ 'accountId': ?0, 'amount': { $gt: ?1 }, 'status': 'SUCCESS' }")
List<Transaction> findSuccessfulAboveThreshold(
String accountId, BigDecimal threshold
);
@Query(value = "{ 'channel': ?0 }", fields = "{ 'amount': 1, 'txnDate': 1 }")
List<Transaction> findAmountAndDateByChannel(String channel);
}
How do you build a custom repository with MongoTemplate?
For aggregation pipelines or any query that derived methods and @Query cannot express, you write a custom repository implementation. You declare a custom interface, implement it in a class whose name ends with Impl, and inject MongoTemplate. Spring automatically wires the custom implementation into the main repository.
package com.bank.repository;
import com.bank.model.MonthlySummary;
import java.util.List;
public interface TransactionRepositoryCustom {
List<MonthlySummary> getMonthlySummary(String accountId);
}
package com.bank.repository;
import com.bank.model.MonthlySummary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.query.Criteria;
import java.util.List;
public class TransactionRepositoryImpl implements TransactionRepositoryCustom {
@Autowired
private MongoTemplate mongoTemplate;
@Override
public List<MonthlySummary> getMonthlySummary(String accountId) {
Aggregation agg = Aggregation.newAggregation(
Aggregation.match(Criteria.where("accountId").is(accountId)),
Aggregation.group("$txnMonth")
.sum("amount").as("totalSpent")
.count().as("txnCount"),
Aggregation.sort(Sort.Direction.DESC, "totalSpent")
);
AggregationResults<MonthlySummary> results =
mongoTemplate.aggregate(agg, "transactions", MonthlySummary.class);
return results.getMappedResults();
}
}
How is a derived method different from @Query and MongoTemplate?
| Feature | Derived Method | @Query | MongoTemplate |
|---|---|---|---|
| Query source | Method name parsed by Spring | Raw JSON filter string | Programmatic fluent API |
| Aggregation support | No | No | Yes |
| Dynamic conditions | No | Limited with SpEL | Full flexibility |
| Readability | High for simple queries | Medium | Lower but most powerful |
| Compile-time safety | Method name checked at startup | JSON string not checked | Criteria API is type-safe |
| Use when | Simple field-based filters | Method name too verbose | Aggregation, joins, complex logic |
// Derived: simple and readable
List<Transaction> findByAccountId(String accountId);
// @Query: when the method name would be unreadable
@Query("{ 'accountId': ?0, 'amount': { $gte: ?1, $lte: ?2 } }")
List<Transaction> findInAmountRange(String accountId, BigDecimal min, BigDecimal max);
// MongoTemplate: for aggregation pipelines
List<MonthlySummary> getMonthlySummary(String accountId);
What are common mistakes when writing MongoDB queries?
The most frequent mistakes involve the naming convention for custom repositories, forgetting to handle BigDecimal serialization, and misunderstanding how $group handles nulls.
// WRONG: class name does not end with Impl
// Spring will NOT autowire this into the main repository
public class CustomTransactionRepo implements TransactionRepositoryCustom {
// This class is invisible to Spring Data
}
// CORRECT: class name = repository name + "Impl"
public class TransactionRepositoryImpl implements TransactionRepositoryCustom {
// Spring Data finds and wires this automatically
}
// WRONG: grouping by a field that can be null
// Documents with null accountId are grouped into a single null bucket
db.transactions.aggregate([
{ $group: { _id: "$accountId", total: { $sum: "$amount" } } }
]);
// Output includes: { "_id": null, "total": 5000 }
// CORRECT: filter out nulls before grouping
db.transactions.aggregate([
{ $match: { accountId: { $ne: null } } },
{ $group: { _id: "$accountId", total: { $sum: "$amount" } } }
]);
What about performance and indexing?
The aggregation pipeline benefits enormously from indexes. A $match stage at the beginning of the pipeline can use an index to avoid a full collection scan. Pankaj always places the $match stage first in the National Bank of India pipelines so the index filters documents before the expensive $group stage processes them.
- Place
$matchand$sortstages as early as possible in the pipeline. - Create a compound index on the fields used in
$matchand$sort. - Use
explain("executionStats")to verify that the pipeline uses the index. - Avoid
$unwindon large arrays unless absolutely necessary, because it multiplies documents.
// Create an index to support the monthly summary pipeline
db.transactions.createIndex(
{ accountId: 1, txnDate: -1 }
);
// Verify the pipeline uses the index
db.transactions.explain("executionStats").aggregate([
{ $match: { accountId: "SAV2001" } },
{ $group: { _id: { $month: "$txnDate" }, total: { $sum: "$amount" } } }
]);
What should a senior engineer know?
A senior engineer should understand the trade-offs between the three query layers and choose the right one for each use case. Derived methods are best for simple CRUD filters on a few fields. @Query is best when the method name would become unreadable but the query is still a simple filter. MongoTemplate with aggregation is best for multi-stage server-side computation.
- The
Implsuffix convention is configurable via@EnableMongoRepositories(repositoryImplementationPostfix = "Custom"), but the defaultImplis universally understood. - For money fields, configure a custom
BigDecimaltoDecimal128converter so MongoDB stores exact decimal values, not floating-point doubles. - In production, set
allowDiskUse: trueon large aggregation pipelines so stages that exceed the 100 MB memory limit spill to disk instead of failing. - Use
$facetto run multiple aggregation pipelines in parallel within a single query, which is useful for dashboard endpoints that need several summaries at once.
What are common interview follow-up questions?
- How does
$lookupdiffer from a SQL JOIN? ($lookupis a left outer join; there is no inner join stage.) - Can you use transactions in MongoDB? (Yes, multi-document transactions since MongoDB 4.0 for replica sets and 4.2 for sharded clusters.)
- How do you handle schema migration in MongoDB? (Schema versioning field, or gradual migration with application-level handling of old and new shapes.)
- What is the difference between
findandaggregatefor a simple filter? (findis optimized for simple filters;aggregatehas pipeline overhead but supports stages.)
Visual Overview
┌─────────────────────────────────────────────────┐
│ MongoDB Query Layers │
├─────────────────────────────────────────────────┤
│ │
│ Layer 1: Shell │
│ db.collection.find({ filter }) │
│ db.collection.aggregate([ stages ]) │
│ │
│ Layer 2: Spring Data │
│ ┌───────────────────────────────────────┐ │
│ │ Derived Methods │ │
│ │ findByAccountIdAndAmountGreaterThan │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ @Query (raw JSON filter) │ │
│ │ @Query("{ 'accountId': ?0 }") │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ MongoTemplate (full control) │ │
│ │ Aggregation.newAggregation(...) │ │
│ └───────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
Aggregation Pipeline Flow:
Documents ─→ $match ─→ $group ─→ $sort ─→ $project ─→ Result
(filter) (bucket) (order) (reshape)
Key Takeaway
MongoDB gives you three layers for custom queries, and in a real banking system you use all three depending on the complexity of the task. In the shell, the raw tools are find with a filter document for simple lookups and aggregate with a pipeline of stages for anything that groups, joins, or computes. The aggregation framework is the workhorse: you chain stages like $match to filter, $group to bucket and sum, $sort to order, and $lookup to join another collection, and all of this runs on the server so only the final small result travels to your application.
In Spring Data MongoDB, the first and easiest approach is derived query methods, where you declare a method like findByAccountIdAndAmountGreaterThan and Spring parses the name and writes the query for you. The second is the @Query annotation, where you provide a raw JSON filter with positional parameters when the method name would get unwieldy. The third, for anything the method name cannot express such as aggregation pipelines, is a custom repository: you declare a custom interface, implement it in a class whose name is the repository name plus Impl so Spring autowires it, and inside that class you use MongoTemplate with the fluent Aggregation.newAggregation API.
At the National Bank of India, Rajesh stored transactions in MongoDB because their shape varies by channel, and the team used derived methods for the relationship team's threshold lookups, @Query for the fraud team's ad hoc filters, and a MongoTemplate aggregation in a custom repository to produce monthly statement summaries. Pankaj's architecture rule was to always place $match first in the pipeline so the index narrows the data before the expensive $group stage, and to create a compound index on the fields used in filtering and sorting.
Remember one sentence: the shell gives you the raw query, the aggregation pipeline does filter-group-compute on the server, and Spring Data gives you derived methods, @Query, or a MongoTemplate custom repository as the complexity grows.
---