advanced~3h

Spring Boot with MongoDB — Spring Data MongoDB & MongoRepository

The same @Transactional, the same repository pattern, the same derived-query-method naming convention as Spring Data JPA — but every one of them compiles down to a BSON document and a Mongo query underneath, not SQL. This chapter is that mapping, made concrete.

Learning objectives

  • Map a Java class to a MongoDB document using @Document, @Id, and @Field.
  • Write MongoRepository derived query methods and know when to reach for @Query instead.
  • Use MongoTemplate for dynamic filters and aggregation pipelines a repository interface can't express.
  • Explain how @Transactional and MongoTransactionManager let repository and MongoTemplate calls share one transaction.
@Document(collection = "orders") public class Order { @Id private String id; // maps to Mongo's _id — String, ObjectId, or any type your driver can convert @Field("customer_id") // stored as customer_id in BSON, but customerId in your Java code private String customerId; private OrderStatus status; private List<LineItem> items; // a nested class maps to a nested/embedded document automatically private Instant createdAt; // getters and setters }

@Document marks a class as mapping to a collection (the collection attribute is optional — Spring Data derives a default from the class name if omitted). @Id marks the field that maps to _id: if you leave it null on save, Spring Data generates an ObjectId for you; if you assign your own value, it's used as-is, letting you use a natural key as _id when that fits your access pattern. @Field lets the BSON field name on disk differ from the Java property name — useful for matching an existing collection's naming convention, or simply keeping camelCase in Java while the stored documents use snake_case.

💻 Code example

@Document(collection = "orders") public class Order { @Id private String id; @Field("customer_id") private String customerId; private OrderStatus status; private List<LineItem> items; private Instant createdAt; }
public interface OrderRepository extends MongoRepository<Order, String> { List<Order> findByCustomerIdAndStatus(String customerId, OrderStatus status); List<Order> findByTotalGreaterThanOrderByCreatedAtDesc(BigDecimal total); long countByStatus(OrderStatus status); }

MongoRepository<Order, String> gives you save(), findById(), findAll(), and deleteById() with no code written at all. The interesting part is method-name parsing: Spring Data reads the method name itself and builds the equivalent Mongo query document — findByCustomerIdAndStatus becomes { customer_id: ?, status: ? }, and OrderByCreatedAtDesc becomes a sort({ createdAt: -1 }). This is the same naming convention family as Spring Data JPA's derived queries, if you already know that — the difference is entirely underneath: here it compiles to a BSON filter document and a Mongo query, not SQL.

💻 Code example

public interface OrderRepository extends MongoRepository<Order, String> { List<Order> findByCustomerIdAndStatus(String customerId, OrderStatus status); List<Order> findByTotalGreaterThanOrderByCreatedAtDesc(BigDecimal total); long countByStatus(OrderStatus status); }
public interface OrderRepository extends MongoRepository<Order, String> { @Query("{ 'status': ?0, 'total': { $gte: ?1 } }") List<Order> findHighValueOrdersByStatus(OrderStatus status, BigDecimal minTotal); @Query("{ 'items': { $elemMatch: { 'sku': ?0, 'qty': { $gt: ?1 } } } }") List<Order> findOrdersWithBulkItem(String sku, int minQty); }

Derived method names (§2) get unwieldy — or simply can't express what you need — once a query involves operators like $elemMatch, deeply nested fields, or a shape the naming convention has no clean word for. @Query takes MongoDB's own query-document syntax directly, with ?0, ?1, ... as positional parameter placeholders. The trade-off is identical to choosing between derived methods and @Query in Spring Data JPA: derived methods are readable and refactor-safe (rename the method, the query updates); @Query gives you the full expressive power of the underlying query language when the naming convention runs out.

💻 Code example

public interface OrderRepository extends MongoRepository<Order, String> { @Query("{ 'status': ?0, 'total': { $gte: ?1 } }") List<Order> findHighValueOrdersByStatus(OrderStatus status, BigDecimal minTotal); @Query("{ 'items': { $elemMatch: { 'sku': ?0, 'qty': { $gt: ?1 } } } }") List<Order> findOrdersWithBulkItem(String sku, int minQty); }

A repository interface's methods are fixed at compile time. A search screen with several optional filters (only some of which the user actually fills in), or an aggregation pipeline (Chapter 01 §4-5), needs a query built up at runtime — that's what MongoTemplate, injected directly, is for.

@Service public class OrderSearchService { private final MongoTemplate mongoTemplate; public OrderSearchService(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } public List<Order> search(String customerId, OrderStatus status, BigDecimal minTotal) { Query query = new Query(); if (customerId != null) query.addCriteria(Criteria.where("customerId").is(customerId)); if (status != null) query.addCriteria(Criteria.where("status").is(status)); if (minTotal != null) query.addCriteria(Criteria.where("total").gte(minTotal)); return mongoTemplate.find(query, Order.class); } // the Chapter 01 §5 top-spending-customers pipeline, expressed via Spring's Aggregation builder public List<Document> topCustomersThisMonth() { Aggregation agg = Aggregation.newAggregation( Aggregation.match(Criteria.where("status").is("DELIVERED")), Aggregation.group("customerId").sum("total").as("totalSpent"), Aggregation.sort(Sort.Direction.DESC, "totalSpent"), Aggregation.limit(10) ); return mongoTemplate.aggregate(agg, "orders", Document.class).getMappedResults(); } }

MongoRepository and MongoTemplate aren't competing tools — most real Spring Data MongoDB codebases use both: repositories for the common, known-in-advance queries, MongoTemplate for the handful of genuinely dynamic or aggregation-shaped ones.

💻 Code example

@Service public class OrderSearchService { private final MongoTemplate mongoTemplate; public OrderSearchService(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } public List<Order> search(String customerId, OrderStatus status, BigDecimal minTotal) { Query query = new Query(); if (customerId != null) query.addCriteria(Criteria.where("customerId").is(customerId)); if (status != null) query.addCriteria(Criteria.where("status").is(status)); if (minTotal != null) query.addCriteria(Criteria.where("total").gte(minTotal)); return mongoTemplate.find(query, Order.class); } public List<Document> topCustomersThisMonth() { Aggregation agg = Aggregation.newAggregation( Aggregation.match(Criteria.where("status").is("DELIVERED")), Aggregation.group("customerId").sum("total").as("totalSpent"), Aggregation.sort(Sort.Direction.DESC, "totalSpent"), Aggregation.limit(10) ); return mongoTemplate.aggregate(agg, "orders", Document.class).getMappedResults(); } }

Transaction Mastery, Chapter 14 §5 already showed the core wiring — a MongoTransactionManager bean and the exact same @Transactional annotation used with JDBC or JPA elsewhere in a Spring app, now backed by MongoDB. That code isn't repeated here; what's worth adding on top of it:

Because MongoRepository and MongoTemplate both ultimately go through the same MongoDatabaseFactory, a single @Transactional service method can freely mix repository calls and MongoTemplate calls and have them automatically share one MongoDB session and transaction — genuinely useful when refactoring a method that started with simple repository calls and later needed a MongoTemplate query added, without having to restructure anything to keep them in the same transaction.

The requirement is unchanged from Transaction Mastery, Chapter 14: the connection must point at a replica set (Chapter 02 §3 covers why, mechanically — the oplog), and — Chapter 03's core point — a well-shaped MongoDB schema often needs @Transactional far less often here than the equivalent relational service layer would, precisely because single-document atomicity covers more of the "must happen together" cases on its own.

Want a visual for this concept?

Generate a diagram tailored to “Spring Boot with MongoDB — Spring Data MongoDB & MongoRepository” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

← Back to all MongoDB chapters