Spring Data MongoDB
|
This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Spring Data MongoDB layers repository abstractions, an object-document mapper, and a template API on top of
the MongoDB Java driver. This page covers the mapping annotations, MongoRepository, indexing, auditing,
transactions, and change streams from the application side; see the
MongoDB Reference for document modeling, indexing strategy, and
aggregation-pipeline concepts, which are not re-explained here.
Dependencies and connection
The spring-boot-starter-data-mongodb starter pulls in the MongoDB driver and Spring Data MongoDB, and
auto-configures a MongoClient and MongoTemplate from application.yml:
spring:
data:
mongodb:
uri: mongodb://localhost:27017/orders_db
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
Document mapping: @Document, @Id, @Field
A mapped domain class is annotated with @Document and maps its fields to a MongoDB document. @Id marks the
primary key (mapped to the BSON _id field), and @Field renames a Java field to a different document field
name:
@Document(collection = "orders")
public class Order {
@Id
private String id; // maps to _id; String, ObjectId, or a custom type
@Field("customer_id")
private String customerId; // stored as "customer_id" in the document
private String status;
private BigDecimal totalAmount;
private Instant placedAt;
private List<LineItem> items; // embedded sub-documents, mapped by convention
// getters and setters omitted
}
public class LineItem {
private String sku;
private int quantity;
private BigDecimal unitPrice;
}
Nested classes such as LineItem are embedded documents by default; a field can instead reference another
collection with @DBRef, though embedding is usually preferred for the reasons covered in the
MongoDB Reference’s data-modeling guide. @Transient excludes a field
from persistence, and @TypeAlias shortens the _class discriminator Spring Data stores for polymorphic types.
Polymorphic documents and inheritance
MongoDB collections are schema-less, so there is no @Inheritance strategy to choose the way there is for JPA
(covered there in depth) — a single orders collection can
already hold documents of different shapes. The problem Spring Data MongoDB actually solves is deserializing
each document back into the right Java subclass. It does this by writing a hidden _class field into every
document, recording the fully-qualified class name, and reading it back to pick the concrete type on the way
out:
public abstract class Payment {
@Id
private String id;
private BigDecimal amount;
}
@TypeAlias("card") // shortens the stored discriminator from the full class name to "card"
public class CardPayment extends Payment {
private String last4Digits;
}
@TypeAlias("bank_transfer")
public class BankTransferPayment extends Payment {
private String iban;
}
public interface PaymentRepository extends MongoRepository<Payment, String> {
List<Payment> findByAmountGreaterThan(BigDecimal threshold);
}
// each element is really a CardPayment or BankTransferPayment instance,
// resolved from the document's stored "_class" discriminator
List<Payment> payments = paymentRepository.findByAmountGreaterThan(BigDecimal.valueOf(100));
@TypeAlias is optional but recommended — without it, the stored discriminator is the full Java class name
(com.example.payments.CardPayment), which breaks deserialization if the class is ever renamed or moved to a
different package. Because there is no schema to enforce column nullability the way SINGLE_TABLE does in JPA,
mixing shapes in one collection costs nothing extra at the storage layer — the entire mechanism exists purely
to get Java polymorphism back on the way out of a schema-less store.
MongoRepository
MongoRepository<T, ID> extends the common Spring Data repository hierarchy with MongoDB-specific operations
(insert, paging, sorting) on top of basic CRUD:
public interface OrderRepository extends MongoRepository<Order, String> {
// derived query -- method name parsed into a MongoDB query
List<Order> findByStatus(String status);
List<Order> findByCustomerIdAndStatus(String customerId, String status);
Optional<Order> findFirstByCustomerIdOrderByPlacedAtDesc(String customerId);
// @Query for anything the derived-query syntax cannot express cleanly
@Query("{ 'totalAmount' : { $gte: ?0 } }")
List<Order> findWithTotalAtLeast(BigDecimal minAmount);
long countByStatus(String status);
}
@Service
public class OrderService {
private final OrderRepository orders;
public OrderService(OrderRepository orders) {
this.orders = orders;
}
public Order place(Order order) {
return orders.save(order); // insert or upsert by _id
}
public Page<Order> forCustomer(String customerId, Pageable pageable) {
return orders.findAll(pageable); // paging/sorting from Spring Data Commons
}
}
save performs an upsert keyed on id; insert always creates a new document and fails on a duplicate _id.
See the Spring Data MongoDB
template API reference for the lower-level operations MongoRepository builds on.
Spring Data MongoDB has supported keyset scrolling natively since 3.1: a derived method returning Window<T> and
taking a ScrollPosition translates to the same range-query pattern as
MongoDB’s own skip() alternative, instead of the
skip()-backed query Page/Pageable runs underneath (see
Keyset scrolling with
Window<T>):
Window<Order> findFirst20ByStatus(String status, Sort sort, ScrollPosition position);
// usage
Window<Order> window = orders.findFirst20ByStatus(
"SHIPPED", Sort.by("id"), ScrollPosition.keyset());
Indexes: @Indexed
@Indexed declares a single-field index directly on the mapped class; @CompoundIndex (at the class level)
declares a multi-field index. Index creation strategy, covered/partial indexes, and text/geospatial indexes are
database-side concerns detailed in the MongoDB Reference: Indexes and
Special Indexes & Search pages:
@Document(collection = "orders")
@CompoundIndex(name = "customer_status_idx", def = "{'customer_id': 1, 'status': 1}")
public class Order {
@Id
private String id;
@Indexed
@Field("customer_id")
private String customerId;
@Indexed(unique = true)
private String orderNumber;
@Indexed(expireAfterSeconds = 2_592_000) // TTL index: auto-delete after 30 days
private Instant placedAt;
private String status;
}
By default Spring Data MongoDB creates declared indexes automatically on startup in development; in production it
is common to disable automatic index creation
(spring.data.mongodb.auto-index-creation=false) and manage indexes explicitly, as covered in the MongoDB
Reference.
Geospatial queries
Spring Data MongoDB layers typed abstractions over the geospatial indexes and operators MongoDB provides at the
database level. See the Special Indexes & Search page for
2dsphere/2d index theory and the underlying $near / $geoWithin / $geoIntersects operators — this
section covers only the Spring Data layer built on top of them, not the operators themselves.
GeoJsonPoint fields and @GeoSpatialIndexed
A GeoJsonPoint field (from org.springframework.data.mongodb.core.geo) maps to a GeoJSON Point document, the
same shape special-indexes-and-search.adoc inserts by hand. @GeoSpatialIndexed declares the backing
2dsphere index, parallel to @Indexed for ordinary indexes above:
@Document(collection = "places")
public class Place {
@Id
private String id;
private String name;
@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
private GeoJsonPoint location;
}
Derived geospatial queries with MongoRepository
MongoRepository extends its derived-query keyword vocabulary with Near and Within, accepting geometry
parameter types such as Point, Sphere, Box, and Polygon. A Near query can additionally take a
Distance and return GeoResults<T>, pairing each match with its computed distance from the query point.
Within generates a $geoWithin query whose operator depends on the geometry type: a Circle produces the
legacy, planar $center operator, which needs a 2d index and rejects the 2dsphere index Place.location
uses above — Sphere (org.springframework.data.mongodb.core.geo.Sphere) is the spherical, 2dsphere-compatible
equivalent, producing $centerSphere:
public interface PlaceRepository extends MongoRepository<Place, String> {
GeoResults<Place> findByLocationNear(Point point, Distance distance);
List<Place> findByLocationWithin(Sphere sphere);
List<Place> findByLocationWithin(Polygon polygon);
}
GeoResults<Place> nearby = placeRepository.findByLocationNear(
new Point(-3.7038, 40.4168), new Distance(5, Metrics.KILOMETERS));
NearQuery and MongoTemplate.geoNear
NearQuery gives the same capability as a dynamic, MongoTemplate-driven query, paralleling this page’s own
"Custom queries with MongoTemplate" pattern for Query/Criteria above. It is built via NearQuery.near(point,
metric) with .maxDistance(…), and executed with MongoTemplate.geoNear(query, EntityClass.class), which
returns GeoResults<T>:
@Repository
public class PlaceQueryRepository {
private final MongoTemplate mongoTemplate;
public PlaceQueryRepository(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public GeoResults<Place> findNearby(Point point, double maxDistanceKm) {
NearQuery query = NearQuery.near(point, Metrics.KILOMETERS)
.maxDistance(maxDistanceKm);
return mongoTemplate.geoNear(query, Place.class);
}
}
Metrics.KILOMETERS / Metrics.MILES passed to NearQuery.near(…) set the unit .maxDistance(double)
interprets its argument in, so callers never convert units manually. See
the
NearQuery API reference and MongoDB’s geospatial
queries overview.
Auditing
@EnableMongoAuditing activates automatic population of creation/modification metadata via marker annotations:
@Configuration
@EnableMongoAuditing
public class MongoConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(Authentication::getName);
}
}
@Document(collection = "orders")
public class Order {
@Id
private String id;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
These fields are populated automatically by Spring Data on save/insert, without any code in the service or
repository layer.
Optimistic locking with @Version
A Long/Integer field annotated @Version protects against lost updates the same way it does everywhere
else in Spring Data (see Spring Data Overview for the
cross-store mechanism):
@Document(collection = "orders")
public class Order {
@Id
private String id;
private String status;
@Version
private Long version;
}
MongoTemplate/MongoRepository’s `save() includes the version it read as part of the update’s filter (an
updateOne with { _id: …, version: <read-value> }) and increments it on success. If another write already
bumped the version in between, the filter matches zero documents, Spring Data detects it, and the save fails
with OptimisticLockingFailureException — the same ObjectOptimisticLockingFailureException family every
other Spring Data module surfaces. As with JPA, application code declares the field and lets Spring Data manage
it; it should never be set manually.
Transactions
MongoDB multi-document transactions (available against a replica set or sharded cluster, as described in the
MongoDB Reference: Transactions page) are driven from Spring with
MongoTransactionManager and the usual @Transactional annotation:
@Configuration
public class TransactionConfig {
@Bean
MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}
@Service
public class OrderTransferService {
private final MongoTemplate mongoTemplate;
public OrderTransferService(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@Transactional
public void moveOrder(String orderId, String fromCustomerId, String toCustomerId) {
Order order = mongoTemplate.findById(orderId, Order.class);
order.setCustomerId(toCustomerId);
mongoTemplate.save(order);
mongoTemplate.updateFirst(
Query.query(Criteria.where("customerId").is(fromCustomerId)),
new Update().inc("openOrderCount", -1),
Customer.class);
// both writes commit or roll back together
}
}
Isolation level
@Transactional’s `isolation attribute has no effect here: MongoTransactionManager neither applies it nor
rejects it — a non-default Isolation value is silently ignored, because MongoDB does not expose the ANSI
isolation levels. Concurrency and visibility for multi-document transactions are governed instead by read
concern, write concern, read preference, and causal consistency — see
MongoDB Reference: Transactions for those settings, and
Transaction Isolation & Locking for how this
compares with the relational stores and with @Version optimistic locking.
Change streams (brief)
Spring Data MongoDB exposes MongoDB’s change streams through ReactiveMongoTemplate.changeStream(…) (or the
imperative MessageListenerContainer with ChangeStreamRequest), letting the application react to inserts,
updates, and deletes as they happen:
@Component
public class OrderChangeListener {
public OrderChangeListener(MongoTemplate mongoTemplate) {
MessageListenerContainer container = new DefaultMessageListenerContainer(mongoTemplate);
container.start();
ChangeStreamRequest<Order> request = ChangeStreamRequest.builder((Order order) ->
System.out.println("Order changed: " + order))
.collection("orders")
.filter(Aggregation.newAggregation(
Aggregation.match(Criteria.where("operationType").is("update"))))
.build();
container.register(request, Order.class);
}
}
Change streams require a replica set or sharded cluster and depend on the oplog; the underlying mechanism, resume tokens, and operational considerations are covered in MongoDB Reference: Change Streams.
Custom queries with MongoTemplate
MongoRepository derived and @Query-annotated methods cover most cases, but MongoTemplate gives full control
for dynamic queries, aggregation pipelines, and raw driver access.
The Query/Criteria fluent builder
Query and Criteria build MongoDB queries programmatically, composing conditions that would otherwise require
hand-written JSON:
@Repository
public class OrderQueryRepository {
private final MongoTemplate mongoTemplate;
public OrderQueryRepository(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public List<Order> findRecentHighValueOrders(String status, BigDecimal minAmount, Instant since) {
Query query = new Query(Criteria.where("status").is(status)
.and("totalAmount").gte(minAmount)
.and("placedAt").gte(since))
.with(Sort.by(Sort.Direction.DESC, "placedAt"))
.limit(50);
return mongoTemplate.find(query, Order.class);
}
public void markShipped(String orderId) {
Query query = Query.query(Criteria.where("id").is(orderId));
Update update = new Update().set("status", "SHIPPED").set("shippedAt", Instant.now());
mongoTemplate.updateFirst(query, update, Order.class);
}
}
The Aggregation Framework via MongoTemplate.aggregate
MongoTemplate.aggregate(…) runs a multi-stage aggregation pipeline and maps the result documents to a target
type; see the MongoDB Reference: Aggregation Pipeline page for
the stage-by-stage semantics:
public List<CustomerTotals> totalsByCustomer(Instant since) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("placedAt").gte(since)),
Aggregation.group("customerId")
.sum("totalAmount").as("total")
.count().as("orderCount"),
Aggregation.project("total", "orderCount").and("_id").as("customerId"),
Aggregation.sort(Sort.Direction.DESC, "total"));
return mongoTemplate.aggregate(aggregation, "orders", CustomerTotals.class).getMappedResults();
}
public class CustomerTotals {
private String customerId;
private BigDecimal total;
private long orderCount;
}
The execute/CollectionCallback escape hatch
When neither the repository abstraction nor Query/Aggregation expose a needed driver feature,
MongoTemplate.execute(…) with a CollectionCallback (or DbCallback) drops down to the raw
com.mongodb.client.MongoCollection:
public long estimatedOrderCount() {
return mongoTemplate.execute("orders", (CollectionCallback<Long>) collection ->
collection.estimatedDocumentCount());
}
public Document rawFindOne(String orderId) {
return mongoTemplate.execute("orders", (CollectionCallback<Document>) collection ->
collection.find(Filters.eq("_id", orderId)).first());
}
This escape hatch is useful for driver-level operations (bulk write builders, collection-level options, index
management calls) that Spring Data does not wrap directly, and shares the same underlying MongoClient and
document mapping as the repository and Query/Aggregation APIs above — a repository for simple lookups and a
MongoTemplate-based class for dynamic queries or reporting is a common split within the same application. See
Spring Data
MongoDB: template query operations for the full set of MongoTemplate query, update, and aggregation methods.