Spring Boot
MongoDB Integration
MongoDB is a document-oriented NoSQL database that stores data as flexible JSON-like BSON documents. Spring Boot integrates with MongoDB through Spring Data MongoDB, which provides MongoRepository, MongoTemplate, and reactive support. It is well-suited for applications with variable-structure data, nested documents, and high write throughput.
Dependencies and Setup
spring-boot-starter-data-mongodb auto-configures a MongoClient, MongoDatabaseFactory, MongoTemplate, and MongoRepository support. The only required configuration is the connection URI.
XML
<!-- pom.xml: -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
# ── application.yml — minimum configuration: ─────────────────────────
spring:
data:
mongodb:
uri: mongodb://localhost:27017/mydb
# Or use separate properties:
host: localhost
port: 27017
database: mydb
username: appuser
password: ${MONGO_PASSWORD}
authentication-database: admin
# ── Production URI (MongoDB Atlas): ──────────────────────────────────
spring:
data:
mongodb:
uri: >-
mongodb+srv://appuser:${MONGO_PASSWORD}@cluster0.abc123.mongodb.net/
mydb?retryWrites=true&w=majority&tls=true
# ── Connection pool tuning: ───────────────────────────────────────────
spring:
data:
mongodb:
uri: mongodb://localhost:27017/mydb
# Advanced pool settings via MongoClientSettings bean (see configuration section)Document Mapping
Spring Data MongoDB maps Java classes to MongoDB documents using @Document, @Id, @Field, and @DBRef annotations. Documents are flexible — not all fields need to be present in every document.
Java
@Document(collection = "products") // MongoDB collection name
@TypeAlias("product") // stored in _class field for polymorphism
public class Product {
@Id // maps to MongoDB _id field
private String id; // String, ObjectId, or UUID
@Field("product_name") // custom field name in MongoDB
private String name;
@Field
private String description;
@Field
private BigDecimal price;
@Indexed // creates a MongoDB index
private String category;
@Indexed(unique = true)
private String sku;
// Nested document — embedded as a sub-document (no @DBRef):
private Address warehouseAddress;
// List of embedded documents:
private List<ProductVariant> variants = new ArrayList<>();
// Map field:
private Map<String, String> attributes = new HashMap<>();
// @DBRef — reference to another collection (like a FK, but stored as ObjectId):
@DBRef
private Category categoryRef; // stored as { $ref: "categories", $id: ObjectId }
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@Version // optimistic locking
private Long version;
}
// ── Embedded document — no @Document annotation: ──────────────────────
public class ProductVariant {
private String sku;
private String colour;
private String size;
private BigDecimal priceAdjustment;
// getters + setters
}
// ── Enable auditing: ──────────────────────────────────────────────────
@SpringBootApplication
@EnableMongoAuditing
public class MyApplication { }MongoRepository
MongoRepository extends CrudRepository with MongoDB-specific operations. Derived query methods work the same as in JPA — Spring Data parses the method name and generates the MongoDB query.
Java
@Repository
public interface ProductRepository extends MongoRepository<Product, String> {
// ── Derived queries — same naming conventions as JPA: ─────────────
List<Product> findByCategory(String category);
Optional<Product> findBySku(String sku);
List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
List<Product> findByCategoryAndPriceLessThan(String category, BigDecimal max);
List<Product> findByNameContainingIgnoreCase(String fragment);
List<Product> findByAttributesKey(String key); // map key
List<Product> findByVariantsSku(String sku); // nested field
// ── With pagination and sorting: ──────────────────────────────────
Page<Product> findByCategory(String category, Pageable pageable);
List<Product> findByActiveTrueOrderByPriceAsc();
// ── Existence and count: ──────────────────────────────────────────
boolean existsBySku(String sku);
long countByCategory(String category);
// ── @Query with MongoDB JSON query syntax: ────────────────────────
@Query("{ 'price': { $gte: ?0, $lte: ?1 }, 'active': true }")
List<Product> findActiveInPriceRange(BigDecimal min, BigDecimal max);
@Query("{ 'attributes.?0': { $exists: true } }")
List<Product> findByAttributeExists(String attributeKey);
@Query(value = "{ 'category': ?0 }",
fields = "{ 'name': 1, 'price': 1, 'sku': 1 }") // projection
List<Product> findSummaryByCategory(String category);
// ── Update query: ─────────────────────────────────────────────────
@Query("{ 'category': ?0 }")
@Update("{ $set: { 'active': ?1 } }")
@Modifying
void updateActiveByCategory(String category, boolean active);
}MongoTemplate — Complex Queries
MongoTemplate provides full programmatic access to MongoDB operations — complex aggregations, dynamic queries, bulk writes, and operations that the repository abstraction does not support.
Java
@Service
@RequiredArgsConstructor
public class ProductService {
private final MongoTemplate mongoTemplate;
// ── Dynamic query with Criteria: ──────────────────────────────────
public Page<Product> search(ProductSearchRequest req, Pageable pageable) {
Criteria criteria = new Criteria();
if (req.name() != null)
criteria.and("name").regex(req.name(), "i"); // case-insensitive
if (req.category() != null)
criteria.and("category").is(req.category());
if (req.minPrice() != null || req.maxPrice() != null) {
Criteria priceCriteria = Criteria.where("price");
if (req.minPrice() != null) priceCriteria.gte(req.minPrice());
if (req.maxPrice() != null) priceCriteria.lte(req.maxPrice());
criteria.andOperator(priceCriteria);
}
if (req.tags() != null && !req.tags().isEmpty())
criteria.and("tags").in(req.tags());
Query query = Query.query(criteria)
.with(pageable)
.with(Sort.by(Sort.Direction.DESC, "createdAt"));
List<Product> content = mongoTemplate.find(query, Product.class);
long total = mongoTemplate.count(Query.query(criteria), Product.class);
return new PageImpl<>(content, pageable, total);
}
// ── Aggregation pipeline: ─────────────────────────────────────────
public List<CategorySummary> getCategorySummaries() {
Aggregation agg = Aggregation.newAggregation(
Aggregation.match(Criteria.where("active").is(true)),
Aggregation.group("category")
.count().as("productCount")
.avg("price").as("averagePrice")
.min("price").as("minPrice")
.max("price").as("maxPrice"),
Aggregation.sort(Sort.by(Sort.Direction.DESC, "productCount")),
Aggregation.limit(10),
Aggregation.project("productCount", "averagePrice", "minPrice", "maxPrice")
.and("_id").as("category")
);
return mongoTemplate.aggregate(agg, "products", CategorySummary.class)
.getMappedResults();
}
// ── Upsert: ───────────────────────────────────────────────────────
public void upsertProduct(Product product) {
Query query = Query.query(Criteria.where("sku").is(product.getSku()));
Update update = Update.update("name", product.getName())
.set("price", product.getPrice())
.set("updatedAt", LocalDateTime.now())
.setOnInsert("createdAt", LocalDateTime.now());
mongoTemplate.upsert(query, update, Product.class);
}
// ── Bulk write: ───────────────────────────────────────────────────
public BulkWriteResult bulkUpsert(List<Product> products) {
BulkOperations ops = mongoTemplate.bulkOps(
BulkOperations.BulkMode.UNORDERED, Product.class);
products.forEach(p -> {
Query q = Query.query(Criteria.where("sku").is(p.getSku()));
Update u = Update.update("name", p.getName()).set("price", p.getPrice());
ops.upsert(q, u);
});
return ops.execute();
}
}Indexes and Configuration
MongoDB indexes are critical for query performance. Spring Data MongoDB creates indexes declared on entity fields. More complex indexes — compound, text, TTL — are best created via Flyway-equivalent migration scripts or programmatically at startup.
Java
// ── Index annotations on the entity: ────────────────────────────────
@Document(collection = "products")
@CompoundIndex(name = "idx_category_price",
def = "{'category': 1, 'price': -1}")
@CompoundIndex(name = "idx_text_search",
def = "{'name': 'text', 'description': 'text'}")
public class Product {
@Id private String id;
@Indexed(unique = true)
private String sku;
@Indexed
private String category;
@Indexed(expireAfterSeconds = 86400) // TTL index — document auto-deleted after 1 day
private LocalDateTime expiresAt;
}
// ── Programmatic index creation at startup: ───────────────────────────
@Configuration
@RequiredArgsConstructor
public class MongoIndexConfig {
private final MongoTemplate mongoTemplate;
@PostConstruct
public void createIndexes() {
IndexOperations ops = mongoTemplate.indexOps(Product.class);
ops.ensureIndex(new Index("sku", Sort.Direction.ASC).unique());
ops.ensureIndex(new Index("category", Sort.Direction.ASC));
ops.ensureIndex(new CompoundIndexDefinition(
new Document("category", 1).append("price", -1)));
}
}
// ── MongoClientSettings — advanced connection configuration: ──────────
@Configuration
public class MongoConfig extends AbstractMongoClientConfiguration {
@Override
protected String getDatabaseName() { return "mydb"; }
@Override
public MongoClient mongoClient() {
return MongoClients.create(MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(
"mongodb://localhost:27017/mydb"))
.applyToConnectionPoolSettings(pool -> pool
.maxSize(20)
.minSize(5)
.maxWaitTime(3, TimeUnit.SECONDS)
.maxConnectionIdleTime(10, TimeUnit.MINUTES))
.applyToSocketSettings(socket -> socket
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS))
.build());
}
}
# ── Docker Compose: ───────────────────────────────────────────────────
services:
mongodb:
image: mongo:7
container_name: myapp-mongo
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: rootpassword
MONGO_INITDB_DATABASE: mydb
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db