MongoDB #
Modern applications often deal with data whose structure isn’t uniform — user profiles with different attributes, event logs whose format keeps changing, or product catalogs with hundreds of variations. Rigid SQL and relational schemas make data model changes slow and expensive. MongoDB offers a solution: a document-based database that stores data in a JSON-like format (BSON), so data structures can change without requiring schema migrations. This article covers how to use MongoDB in Java — from setting up a connection with the MongoDB Java Driver, CRUD operations, queries and filters, the aggregation pipeline, to Spring Data MongoDB integration for more idiomatic code.
MongoDB Basics #
Before writing code, it’s important to understand how MongoDB organizes data. MongoDB uses terminology different from relational databases.
| Relational DB | MongoDB | Description |
|---|---|---|
| Database | Database | Same — a container for collections |
| Table | Collection | A group of documents |
| Row | Document | A single data entry in BSON format |
| Column | Field | An attribute within a document |
| Primary Key | _id | Unique identifier, auto-generated if not provided |
| JOIN | $lookup | Aggregation to combine data between collections |
| Index | Index | Same — speeds up queries |
MongoDB documents are in BSON (Binary JSON), which supports richer data types than regular JSON: ObjectId, Date, Decimal128, nested arrays, and nested documents.
// Example MongoDB document structure
{
"_id": ObjectId("64f3a1b2c3d4e5f6a7b8c9d0"),
"name": "Pro X Laptop",
"price": 15000000,
"specifications": {
"ram": "16GB",
"storage": "512GB SSD",
"screen": "15.6 inch"
},
"tags": ["laptop", "gaming", "high-end"],
"available": true,
"created_at": ISODate("2024-01-15T08:00:00Z")
}
Note that a document can have nested fields (specifications) and array fields (tags) natively, without needing extra relational tables.
Installation and Dependencies #
The MongoDB Java Driver is available in two variants: the sync driver (blocking) and the reactive driver (non-blocking). This article focuses on the more commonly used sync driver.
Add the following dependency to your pom.xml (Maven):
<!-- MongoDB Java Driver -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>4.11.1</version>
</dependency>
<!-- If using Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
Or with Gradle:
// build.gradle
implementation 'org.mongodb:mongodb-driver-sync:4.11.1'
// If using Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
The MongoDB Java Driver version 4.x and above uses a different API from version 3.x. Make sure you use version 4.x because version 3.x is end-of-life.
Connecting to MongoDB #
Connections to MongoDB are made through MongoClient, which is thread-safe and can be shared across the entire application. Don’t create a new MongoClient on every request.
Connecting with a Connection String #
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
public class MongoDBConnection {
// ANTI-PATTERN: creating a new MongoClient in every method
// This wastes connections and slows the application down
public void antiPattern() {
MongoClient client = MongoClients.create("mongodb://localhost:27017");
// do something
client.close(); // must always be closed, easy to forget
}
// CORRECT: create the MongoClient once as a singleton or bean
private static final MongoClient CLIENT =
MongoClients.create("mongodb://localhost:27017");
public MongoDatabase getDatabase() {
return CLIENT.getDatabase("online_store");
}
}
Connection String with Authentication #
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClients;
public class MongoDBConfig {
public MongoClient createClient() {
// Format: mongodb://username:***@host:port/database
String connectionString = "mongodb://admin:***@localhost:27017/online_store";
MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(connectionString))
.applyToConnectionPoolSettings(builder ->
builder.maxSize(20) // max 20 connections in the pool
.minSize(5)) // min 5 active connections
.applyToSocketSettings(builder ->
builder.connectTimeout(5000, java.util.concurrent.TimeUnit.MILLISECONDS))
.build();
return MongoClients.create(settings);
}
}
Connection Architecture #
flowchart TD
A[Java Application] --> B[MongoClient Singleton]
B --> C[Connection Pool]
C --> D[MongoDB Server :27017]
D --> E[(Database: online_store)]
E --> F[(Collection: products)]
E --> G[(Collection: users)]
E --> H[(Collection: orders)]CRUD Operations #
All data operations in MongoDB work on MongoCollection<Document>. The Java driver uses Document as the representation of a BSON document.
Create — Saving Documents #
import com.mongodb.client.MongoCollection;
import com.mongodb.client.result.InsertOneResult;
import com.mongodb.client.result.InsertManyResult;
import org.bson.Document;
import org.bson.types.ObjectId;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class ProductRepository {
private final MongoCollection<Document> collection;
public ProductRepository(MongoDatabase database) {
this.collection = database.getCollection("products");
}
// Insert a single document
public ObjectId saveProduct(String name, int price, String category) {
Document document = new Document()
.append("name", name)
.append("price", price)
.append("category", category)
.append("stock", 0)
.append("active", true)
.append("created_at", new Date());
InsertOneResult result = collection.insertOne(document);
// _id is auto-generated if not provided
return (ObjectId) result.getInsertedId().asObjectId().getValue();
}
// Insert many documents at once
public void saveManyProducts(List<Document> productList) {
InsertManyResult result = collection.insertMany(productList);
System.out.println("Successfully saved " + result.getInsertedIds().size() + " products");
}
}
Read — Reading Documents #
Queries in MongoDB use Filters from the com.mongodb.client.model package. Avoid building filters manually with Document because it’s error-prone and hard to read.
import com.mongodb.client.FindIterable;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import com.mongodb.client.model.Sorts;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import java.util.ArrayList;
import java.util.List;
public class ProductQuery {
private final MongoCollection<Document> collection;
public ProductQuery(MongoCollection<Document> collection) {
this.collection = collection;
}
// Find by ID
public Document findById(String id) {
Bson filter = Filters.eq("_id", new ObjectId(id));
return collection.find(filter).first();
}
// Find all active products within a price range
public List<Document> findActiveProducts(int minPrice, int maxPrice) {
Bson filter = Filters.and(
Filters.eq("active", true),
Filters.gte("price", minPrice),
Filters.lte("price", maxPrice)
);
// Projection: take only the needed fields
Bson projection = Projections.fields(
Projections.include("name", "price", "category"),
Projections.excludeId()
);
return collection.find(filter)
.projection(projection)
.sort(Sorts.ascending("price"))
.limit(20)
.into(new ArrayList<>());
}
// Search with text search (requires a text index)
public List<Document> searchByKeyword(String keyword) {
Bson filter = Filters.text(keyword);
return collection.find(filter)
.sort(Sorts.metaTextScore("score"))
.into(new ArrayList<>());
}
}
Update — Updating Documents #
import com.mongodb.client.model.Updates;
import com.mongodb.client.result.UpdateResult;
import java.util.Date;
public class ProductUpdate {
private final MongoCollection<Document> collection;
public ProductUpdate(MongoCollection<Document> collection) {
this.collection = collection;
}
// Update a single field
public boolean updatePrice(String id, int newPrice) {
Bson filter = Filters.eq("_id", new ObjectId(id));
Bson update = Updates.combine(
Updates.set("price", newPrice),
Updates.set("updated_at", new Date())
);
UpdateResult result = collection.updateOne(filter, update);
return result.getModifiedCount() > 0;
}
// Increment a numeric value
public boolean addStock(String id, int amount) {
Bson filter = Filters.eq("_id", new ObjectId(id));
Bson update = Updates.inc("stock", amount); // atomic, safe for concurrent updates
UpdateResult result = collection.updateOne(filter, update);
return result.getModifiedCount() > 0;
}
// Update many documents at once
public long deactivateCategory(String category) {
Bson filter = Filters.eq("category", category);
Bson update = Updates.set("active", false);
UpdateResult result = collection.updateMany(filter, update);
return result.getModifiedCount();
}
// Upsert: update if it exists, insert if it doesn't
public void upsertProduct(String sku, String name, int price) {
Bson filter = Filters.eq("sku", sku);
Bson update = Updates.combine(
Updates.setOnInsert("created_at", new Date()),
Updates.set("name", name),
Updates.set("price", price),
Updates.set("updated_at", new Date())
);
com.mongodb.client.model.UpdateOptions options =
new com.mongodb.client.model.UpdateOptions().upsert(true);
collection.updateOne(filter, update, options);
}
}
Avoid usingreplaceOne()if you only want to change a few fields.replaceOne()replaces the entire document, including fields you didn’t include — the old fields will be lost.
Delete — Deleting Documents #
import com.mongodb.client.result.DeleteResult;
public class ProductDelete {
private final MongoCollection<Document> collection;
public ProductDelete(MongoCollection<Document> collection) {
this.collection = collection;
}
// Delete a single document
public boolean deleteProduct(String id) {
Bson filter = Filters.eq("_id", new ObjectId(id));
DeleteResult result = collection.deleteOne(filter);
return result.getDeletedCount() > 0;
}
// Delete many documents
public long deleteInactiveProducts() {
Bson filter = Filters.eq("active", false);
DeleteResult result = collection.deleteMany(filter);
return result.getDeletedCount();
}
}
Advanced Queries #
MongoDB supports queries far more expressive than simple equality checks. Some query patterns commonly used in production applications:
Queries on Nested Fields and Arrays #
public class AdvancedQuery {
private final MongoCollection<Document> collection;
public AdvancedQuery(MongoCollection<Document> collection) {
this.collection = collection;
}
// Query nested fields using dot notation
public List<Document> findLaptopsWith16GB() {
// "specifications.ram" refers to the ram field inside the specifications object
Bson filter = Filters.eq("specifications.ram", "16GB");
return collection.find(filter).into(new ArrayList<>());
}
// Query documents whose array contains a certain value
public List<Document> findProductsWithTag(String tag) {
// $elemMatch isn't needed for arrays of primitives
Bson filter = Filters.eq("tags", tag);
return collection.find(filter).into(new ArrayList<>());
}
// Query using $in — find products from any category in the list
public List<Document> findProductsByCategory(List<String> categoryList) {
Bson filter = Filters.in("category", categoryList);
return collection.find(filter).into(new ArrayList<>());
}
// Query by field existence
public List<Document> findProductsWithDiscount() {
Bson filter = Filters.exists("discount");
return collection.find(filter).into(new ArrayList<>());
}
// Query with regex
public List<Document> findProductsByName(String keyword) {
// Case-insensitive search on the name field
Bson filter = Filters.regex("name", keyword, "i");
return collection.find(filter).into(new ArrayList<>());
}
}
Query Flow Sequence #
sequenceDiagram
participant App as Java Application
participant Driver as MongoDB Driver
participant DB as MongoDB Server
App->>Driver: collection.find(filter)
Driver->>DB: Send BSON query
DB->>DB: Scan index (if any)
DB->>DB: Filter documents
DB-->>Driver: Cursor
Driver-->>App: FindIterable<Document>
App->>Driver: .sort().limit().into()
Driver->>DB: Fetch document batch
DB-->>Driver: BSON documents
Driver-->>App: List<Document>Aggregation Pipeline #
Aggregation is MongoDB’s most powerful feature. The pipeline processes documents through a series of stages — each stage transforms, filters, or groups documents before sending them to the next stage.
Pipeline Concept #
flowchart LR
A[(Collection)] --> B["$match\nFilter documents"]
B --> C["$group\nGroup & count"]
C --> D["$sort\nSort results"]
D --> E["$limit\nLimit the count"]
E --> F[Final Result]Aggregation Examples #
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.model.Accumulators;
import com.mongodb.client.model.Aggregates;
import com.mongodb.client.model.BsonField;
import org.bson.Document;
import java.util.Arrays;
import java.util.List;
public class ProductAggregation {
private final MongoCollection<Document> collection;
public ProductAggregation(MongoCollection<Document> collection) {
this.collection = collection;
}
// Count total products and the average price per category
public List<Document> statisticsPerCategory() {
List<Bson> pipeline = Arrays.asList(
// Stage 1: Filter only active products
Aggregates.match(Filters.eq("active", true)),
// Stage 2: Group by category
Aggregates.group(
"$category",
Accumulators.sum("total_products", 1),
Accumulators.avg("average_price", "$price"),
Accumulators.min("cheapest_price", "$price"),
Accumulators.max("most_expensive_price", "$price")
),
// Stage 3: Sort by total products (descending)
Aggregates.sort(Sorts.descending("total_products")),
// Stage 4: Take the top 10 categories
Aggregates.limit(10)
);
return collection.aggregate(pipeline).into(new ArrayList<>());
}
// $lookup: join with another collection
public List<Document> productsWithSellerDetails() {
List<Bson> pipeline = Arrays.asList(
Aggregates.lookup(
"sellers", // the collection being joined
"seller_id", // local field
"_id", // field in the target collection
"seller_info" // name of the join result field
),
// Unwind: split the lookup result array into separate documents
Aggregates.unwind("$seller_info"),
// Project: specify the returned fields
Aggregates.project(
Projections.fields(
Projections.include("name", "price"),
Projections.computed("seller_name", "$seller_info.name"),
Projections.computed("seller_city", "$seller_info.city")
)
)
);
return collection.aggregate(pipeline).into(new ArrayList<>());
}
// $facet: run multiple pipelines at once (useful for faceted search)
public Document facetedSearch(String category, int minPrice, int maxPrice) {
Bson matchFilter = Filters.and(
Filters.eq("category", category),
Filters.gte("price", minPrice),
Filters.lte("price", maxPrice)
);
List<Bson> pipeline = Arrays.asList(
Aggregates.match(matchFilter),
Aggregates.facet(
new com.mongodb.client.model.Facet("results",
Aggregates.sort(Sorts.ascending("price")),
Aggregates.limit(20)
),
new com.mongodb.client.model.Facet("total",
Aggregates.count("count")
),
new com.mongodb.client.model.Facet("price_range",
Aggregates.group(null,
Accumulators.min("min", "$price"),
Accumulators.max("max", "$price")
)
)
)
);
return collection.aggregate(pipeline).first();
}
}
Indexing #
Indexes are the key to MongoDB performance. Without an index, MongoDB must scan the entire collection (a collection scan) for every query — slow once documents reach millions.
Index Types #
| Index Type | Use Case | Example |
|---|---|---|
| Single Field | Queries on one field | { "email": 1 } |
| Compound | Queries on a field combination | { "category": 1, "price": -1 } |
| Text | Full-text search | { "name": "text", "description": "text" } |
| Geospatial | Location-based queries | { "location": "2dsphere" } |
| Partial | Index only a subset of documents | filter: { "active": true } |
| Unique | Ensures unique values | { "email": 1 }, { unique: true } |
Creating Indexes #
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
public class IndexManager {
private final MongoCollection<Document> collection;
public IndexManager(MongoCollection<Document> collection) {
this.collection = collection;
}
public void createIndexes() {
// Single ascending index
collection.createIndex(Indexes.ascending("email"));
// Compound index: sort category ascending, price descending
// The field order in a compound index is very important!
collection.createIndex(
Indexes.compoundIndex(
Indexes.ascending("category"),
Indexes.descending("price")
)
);
// Unique index to ensure no duplicate emails
IndexOptions uniqueOptions = new IndexOptions().unique(true);
collection.createIndex(Indexes.ascending("email"), uniqueOptions);
// Text index for full-text search
collection.createIndex(Indexes.compoundIndex(
Indexes.text("name"),
Indexes.text("description")
));
// Partial index: only index active products
// Saves storage, the index is smaller, queries are faster
IndexOptions partialOptions = new IndexOptions()
.partialFilterExpression(Filters.eq("active", true));
collection.createIndex(
Indexes.ascending("price"),
partialOptions
);
// TTL index: documents are automatically deleted after a certain time
// Useful for sessions, caches, or temporary data
IndexOptions ttlOptions = new IndexOptions().expireAfter(30L, java.util.concurrent.TimeUnit.DAYS);
collection.createIndex(Indexes.ascending("created_at"), ttlOptions);
}
}
Indexes speed up reads but slow down writes because every insert/update must update all indexes. Don’t create indexes for every field — create them only for fields frequently used in query filters.
Spring Data MongoDB Integration #
If your application uses Spring Boot, Spring Data MongoDB provides a higher-level abstraction than the raw MongoDB Java Driver. The code becomes more concise and idiomatic.
Configuration #
# application.yml
spring:
data:
mongodb:
uri: mongodb://localhost:27017/online_store
# Or with authentication:
# uri: mongodb://admin:***@localhost:27017/online_store?authSource=admin
Document Model #
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.index.TextIndexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Document(collection = "products") // the collection name in MongoDB
public class Product {
@Id
private String id; // MongoDB will generate _id as an ObjectId
@TextIndexed // automatically included in the text index for full-text search
private String name;
@Indexed // creates a single field index automatically
private String category;
private BigDecimal price;
private int stock;
private boolean active;
@TextIndexed(weight = 2) // lower weight than name
private String description;
private List<String> tags;
@Field("specifications") // the MongoDB field name can differ from the Java property name
private Specifications detailSpecifications;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Constructor, getters, setters...
public static class Specifications {
private String ram;
private String storage;
private String processor;
// getters and setters...
}
}
Repository #
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
public interface ProductRepository extends MongoRepository<Product, String> {
// Spring Data auto-generates queries from method names
List<Product> findByCategory(String category);
List<Product> findByActiveTrue();
List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
Optional<Product> findByName(String name);
// Built-in pagination and sorting
Page<Product> findByCategoryAndActive(String category, boolean active, Pageable pageable);
// Custom queries with the @Query annotation
@Query("{ 'price': { $gte: ?0, $lte: ?1 }, 'active': true }")
List<Product> searchByPriceRange(BigDecimal min, BigDecimal max);
// Queries with projections — take only certain fields
@Query(value = "{ 'category': ?0 }", fields = "{ 'name': 1, 'price': 1 }")
List<Product> namesAndPricesByCategory(String category);
long countByCategory(String category);
void deleteByActiveFalse();
}
MongoTemplate for Complex Queries #
MongoRepository is suitable for simple queries. For more complex queries — especially aggregation — use MongoTemplate.
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class ProductService {
@Autowired
private MongoTemplate mongoTemplate;
// Queries with dynamic criteria
public List<Product> searchProductsWithFilter(
String category, BigDecimal minPrice, BigDecimal maxPrice) {
Query query = new Query();
// Build criteria dynamically
Criteria criteria = new Criteria();
if (category != null && !category.isEmpty()) {
criteria.and("category").is(category);
}
if (minPrice != null) {
criteria.and("price").gte(minPrice);
}
if (maxPrice != null) {
criteria.and("price").lte(maxPrice);
}
criteria.and("active").is(true);
query.addCriteria(criteria);
return mongoTemplate.find(query, Product.class);
}
// Partial updates
public void updateStock(String id, int stockChange) {
Query query = new Query(Criteria.where("id").is(id));
Update update = new Update()
.inc("stock", stockChange)
.set("updatedAt", LocalDateTime.now());
mongoTemplate.updateFirst(query, update, Product.class);
}
// Aggregation with Spring Data
public List<CategoryStatistics> statisticsPerCategory() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("active").is(true)),
Aggregation.group("category")
.count().as("totalProducts")
.avg("price").as("averagePrice")
.min("price").as("lowestPrice")
.max("price").as("highestPrice"),
Aggregation.sort(org.springframework.data.domain.Sort.by(
org.springframework.data.domain.Sort.Direction.DESC, "totalProducts")),
Aggregation.limit(10)
);
AggregationResults<CategoryStatistics> results =
mongoTemplate.aggregate(aggregation, "products", CategoryStatistics.class);
return results.getMappedResults();
}
// DTO for the aggregation result
public static class CategoryStatistics {
private String id; // the result of $group _id
private int totalProducts;
private double averagePrice;
private BigDecimal lowestPrice;
private BigDecimal highestPrice;
// getters and setters...
}
}
Multi-Document Transactions #
MongoDB has supported ACID transactions since version 4.0, but only on Replica Set or Sharded Cluster deployments — not on standalone instances.
import com.mongodb.client.ClientSession;
import com.mongodb.TransactionOptions;
import com.mongodb.ReadConcern;
import com.mongodb.WriteConcern;
import com.mongodb.ReadPreference;
public class TransactionExample {
private final MongoClient client;
public TransactionExample(MongoClient client) {
this.client = client;
}
// Transaction: decrease product stock and create an order record atomically
public void createOrder(String productId, int quantity, String userId) {
TransactionOptions txnOptions = TransactionOptions.builder()
.readPreference(ReadPreference.primary())
.readConcern(ReadConcern.LOCAL)
.writeConcern(WriteConcern.MAJORITY)
.build();
try (ClientSession session = client.startSession()) {
session.withTransaction(() -> {
MongoDatabase db = client.getDatabase("online_store");
MongoCollection<Document> productsCol = db.getCollection("products");
MongoCollection<Document> ordersCol = db.getCollection("orders");
// Check and decrease the stock
Bson productFilter = Filters.and(
Filters.eq("_id", new ObjectId(productId)),
Filters.gte("stock", quantity) // make sure the stock is sufficient
);
Bson stockUpdate = Updates.inc("stock", -quantity);
Document product = productsCol.findOneAndUpdate(session, productFilter, stockUpdate);
if (product == null) {
throw new RuntimeException("Insufficient stock");
}
// Create the order record
Document order = new Document()
.append("product_id", new ObjectId(productId))
.append("user_id", userId)
.append("quantity", quantity)
.append("unit_price", product.get("price"))
.append("status", "pending")
.append("created_at", new Date());
ordersCol.insertOne(session, order);
return null;
}, txnOptions);
}
}
}
Transactions in MongoDB add performance overhead. Use them only when atomicity is genuinely needed. In many cases, good schema design with embedded documents can avoid the need for transactions.
When to Use MongoDB vs SQL #
Choosing between MongoDB and a relational database isn’t about which is absolutely better — it’s about which fits your use case.
Use MongoDB when:
✓ The data structure isn't uniform or changes frequently
✓ Hierarchical data that's naturally nested
✓ You need horizontal scaling with sharding
✓ The data is document-centric (profiles, logs, catalogs)
✓ There aren't many complex relationships between entities
✓ You need full-text search with MongoDB Atlas Search
Consider SQL (PostgreSQL/MySQL) when:
✗ The data is highly relational with many complex JOINs
✗ You need strict ACID transactions for many operations
✗ The team is more familiar and the application already uses SQL
✗ The data is structured and the schema won't change much
✗ You need complex reporting and analytics (SQL is more expressive)
Summary #
- Document model — MongoDB stores data as BSON documents that can have nested fields and arrays natively, without needing join tables.
- MongoClient is a singleton — create it once and share it across the application. Don’t create a new
MongoClienton every request.- Use
Filters,Updates,Aggregatesfrom thecom.mongodb.client.modelpackage — don’t build queries manually withDocumentbecause it’s error-prone.- The aggregation pipeline is the most powerful feature — use it for grouping, joining between collections (
$lookup), data transformation, and faceted search.- Indexes are the key to performance — without indexes, queries on large collections will be slow. Create indexes only for fields frequently used in filters, not every field.
- Spring Data MongoDB simplifies integration: use
MongoRepositoryfor simple operations andMongoTemplatefor dynamic queries or complex aggregations.- Multi-document transactions are available since MongoDB 4.0, but only on Replica Sets and Sharded Clusters — not standalone. Use them only when atomicity is genuinely needed.
- Choose MongoDB for unstructured, hierarchical, or frequently changing-schema data. Choose SQL for highly relational data with many complex JOINs.