Spring Boot Tutorial
Spring Data MongoDB
Not all data fits neatly into tables. Product catalogues where each category has different attributes, user activity feeds, content management systems and event logs are often a better fit for a document database such as MongoDB, which stores flexible JSON-like documents.
Spring Data MongoDB gives you the same repository programming model you know from JPA — derived queries, pagination, auditing — plus MongoTemplate for advanced queries and aggregation pipelines. This lesson covers modelling documents, repositories, queries, aggregations, indexes and testing with Testcontainers.
Documents vs Rows
A MongoDB document is a BSON (binary JSON) object stored in a collection. Documents in one collection can have different fields, and can embed arrays and sub-documents. Instead of joining tables, you usually embed data that is read together (an order's line items inside the order) and reference data that is shared or grows unbounded (the customer, by id). Design documents around your application's read patterns.
Setup and Mapping
Add spring-boot-starter-data-mongodb and set spring.mongodb.uri (with Docker Compose or Testcontainers this is configured automatically). Annotate classes or records with @Document; use @Id for the identifier (a String maps to MongoDB's ObjectId), @Field to rename fields and @Indexed for indexes. Enable automatic index creation in development, and manage indexes explicitly in production.
Repositories and Queries
MongoRepository supports CRUD, derived queries (findByCategoryAndPriceLessThan), paging and sorting. @Query accepts MongoDB JSON query syntax. For dynamic queries, updates of individual fields, and aggregations, inject MongoTemplate and use the Query, Criteria, Update and Aggregation builders.
Aggregation Pipelines
MongoDB's aggregation framework processes documents through stages — $match, $group, $sort, $project, $unwind, $lookup — similar to SQL's WHERE, GROUP BY and JOIN. Spring's Aggregation API builds pipelines type-safely and maps results to records.
Transactions and Consistency
Single-document writes in MongoDB are always atomic, which is why embedding related data is powerful. Multi-document transactions are supported on replica sets; enable them with a MongoTransactionManager bean and use @Transactional as usual — but design to need them rarely.
Examples
Setup and a document model with embedded data
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
# application.yml
spring:
mongodb:
uri: mongodb://localhost:27017/webnest
@Document(collection = "products")
public record Product(
@Id String id,
@Indexed(unique = true) String sku,
String name,
String category,
BigDecimal price,
List<String> tags,
Map<String, Object> attributes, // flexible per-category attributes
List<Review> reviews) {} // embedded sub-documents
public record Review(String user, int rating, String comment, Instant createdAt) {}
db.products.findOne()
{ _id: ObjectId("66f6..."), sku: "HD-NAVY-M", name: "Webnest Hoodie", category: "apparel",
price: Decimal128("1299.00"), tags: ["hoodie", "merch"],
attributes: { size: "M", color: "navy" },
reviews: [ { user: "asha", rating: 5, comment: "Very warm", createdAt: ISODate("2026-09-20T10:00:00Z") } ] }
Repository with derived queries, JSON @Query and paging
public interface ProductRepository extends MongoRepository<Product, String> {
List<Product> findByCategoryAndPriceLessThanOrderByPriceAsc(String category, BigDecimal max);
Page<Product> findByTagsContaining(String tag, Pageable pageable);
@Query("{ 'attributes.color': ?0, 'price': { $lte: ?1 } }")
List<Product> findByColorUpTo(String color, BigDecimal max);
Optional<Product> findBySku(String sku);
}
productRepository.findByCategoryAndPriceLessThanOrderByPriceAsc("apparel", new BigDecimal("1500"))
.forEach(p -> System.out.println(p.name() + " " + p.price()));
Webnest Cap 499.00
Webnest Tee 799.00
Webnest Hoodie 1299.00
MongoTemplate: partial updates and an aggregation pipeline
@Service
public class ProductAnalytics {
private final MongoTemplate mongo;
public ProductAnalytics(MongoTemplate mongo) {
this.mongo = mongo;
}
// Append a review atomically without loading the whole document
public void addReview(String sku, Review review) {
mongo.updateFirst(
Query.query(Criteria.where("sku").is(sku)),
new Update().push("reviews", review),
Product.class);
}
public record CategoryRating(String category, double avgRating, long reviews) {}
public List<CategoryRating> ratingsByCategory() {
Aggregation pipeline = Aggregation.newAggregation(
Aggregation.unwind("reviews"),
Aggregation.group("category")
.avg("reviews.rating").as("avgRating")
.count().as("reviews"),
Aggregation.project("avgRating", "reviews").and("_id").as("category"),
Aggregation.sort(Sort.Direction.DESC, "avgRating"));
return mongo.aggregate(pipeline, "products", CategoryRating.class).getMappedResults();
}
}
addReview("HD-NAVY-M", ...) -> db.products.updateOne({sku:"HD-NAVY-M"}, {$push:{reviews:{...}}})
ratingsByCategory()
[CategoryRating[category=books, avgRating=4.7, reviews=128],
CategoryRating[category=apparel, avgRating=4.4, reviews=96]]
Integration test with a MongoDB container
@DataMongoTest
@Testcontainers
class ProductRepositoryTest {
@Container
@ServiceConnection
static MongoDBContainer mongo = new MongoDBContainer("mongo:8");
@Autowired ProductRepository repository;
@Test
void findsByColorAndMaxPrice() {
repository.save(new Product(null, "TEE-RED-S", "Tee", "apparel", new BigDecimal("799"),
List.of("tee"), Map.of("color", "red"), List.of()));
assertThat(repository.findByColorUpTo("red", new BigDecimal("1000")))
.extracting(Product::sku)
.containsExactly("TEE-RED-S");
}
}
ProductRepositoryTest > findsByColorAndMaxPrice() PASSED
Common Mistakes
- Copying a relational schema into MongoDB with a collection per table and many manual "joins".
- Embedding arrays that grow without limit (e.g. every page view inside a user document), eventually hitting the 16 MB document limit.
- Relying on auto-index-creation in production instead of managing indexes deliberately.
- Loading, modifying and saving whole documents for small changes, causing lost updates; use MongoTemplate atomic updates.
- Storing money as double instead of BigDecimal/Decimal128.
Key Points to Remember
- MongoDB stores flexible documents; embed data read together and reference shared data by id.
- spring-boot-starter-data-mongodb gives MongoRepository with derived queries and paging.
- MongoTemplate handles dynamic queries, atomic partial updates and aggregation pipelines.
- Single-document writes are atomic; use multi-document transactions sparingly.
- Test with @DataMongoTest and a MongoDBContainer connected via @ServiceConnection.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.