Spring BootMongoDB Integration
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