Back to Aggregation: GROUP BY, HAVING, COUNT, SUM
Q1Question 1 of 10

How do you write a custom method or query in MongoDB (shell, aggregation framework, Spring Data custom repository)?

Key Concepts
  • The mongo shell uses `find` with a JSON filter document and `aggregate` with a pipeline of stages for custom queries.
  • The aggregation framework chains stages like `$match`, `$group`, `$sort`, and `$lookup` to filter, group, and compute on the server.
  • Spring Data MongoDB offers derived query methods where the method name is parsed into a query automatically.
  • The `@Query` annotation accepts a raw JSON filter with positional parameters for queries the method name cannot express.
  • A custom repository implementation using `MongoTemplate` gives full control including aggregation pipelines.
  • The custom implementation class must follow the naming convention of the repository name plus the `Impl` suffix for Spring to autowire it.

Definition

Shell `find`/`aggregate` is the raw query, the aggregation pipeline is server-side filter-group-compute, and Spring Data gives derived methods, `@Query`, or a `MongoTemplate` custom repo for anything complex.
  • Shell queries – the mongo shell provides find with a JSON filter document for simple lookups and aggregate with a pipeline of stages for grouped or computed results.
  • Aggregation framework – a server-side pipeline that chains stages like $match, $group, $sort, and $lookup to filter, join, and compute without pulling raw data to the client.
  • Spring Data derived methods – declaring a method like findByAccountIdAndAmountGreaterThan on a repository interface lets Spring parse the name and build the query automatically.
  • @Query annotation – 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 with MongoTemplate and 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.

JAVASCRIPT
// Shell: find all transactions for account SAV2001 with amount > 50000
db.transactions.find(
  { accountId: "SAV2001", amount: { $gt: 50000 } }
);
TEXT
{ "_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.

  • find handles simple CRUD lookups.
  • aggregate handles multi-stage server-side computation.
  • Spring Data provides a Java-friendly abstraction over both.
JAVASCRIPT
// Shell: project only amount and channel, exclude _id
db.transactions.find(
  { accountId: "SAV2001" },
  { amount: 1, channel: 1, _id: 0 }
);
TEXT
{ "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.
JAVASCRIPT
// 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 } }
]);
TEXT
{ "_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.

JAVA
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.

JAVA
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.

JAVA
package com.bank.repository;

import com.bank.model.MonthlySummary;
import java.util.List;

public interface TransactionRepositoryCustom {
    List<MonthlySummary> getMonthlySummary(String accountId);
}
JAVA
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 |

JAVA
// 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.

JAVA
// 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
}
JAVASCRIPT
// 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 $match and $sort stages as early as possible in the pipeline.
  • Create a compound index on the fields used in $match and $sort.
  • Use explain("executionStats") to verify that the pipeline uses the index.
  • Avoid $unwind on large arrays unless absolutely necessary, because it multiplies documents.
JAVASCRIPT
// 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 Impl suffix convention is configurable via @EnableMongoRepositories(repositoryImplementationPostfix = "Custom"), but the default Impl is universally understood.
  • For money fields, configure a custom BigDecimal to Decimal128 converter so MongoDB stores exact decimal values, not floating-point doubles.
  • In production, set allowDiskUse: true on large aggregation pipelines so stages that exceed the 100 MB memory limit spill to disk instead of failing.
  • Use $facet to 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 $lookup differ from a SQL JOIN? ($lookup is 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 find and aggregate for a simple filter? (find is optimized for simple filters; aggregate has pipeline overhead but supports stages.)

Visual Overview

DIAGRAM
  ┌─────────────────────────────────────────────────┐
  │              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.

---

Share
Use ← → arrow keys to navigate