Mastering MongoTemplate and Criteria API  in Spring

    Mastering MongoTemplate and Criteria API in Spring

    Learn how to use the Criteria API and MongoTemplate to build advanced queries in Spring MongoDB Data.

    default profile

    Anuj Kumar Sharma

    September 13, 2025

    5 min read

    We've come a long way - from basic MongoDB commands to Spring Data repositories, and then to complex relationships. Now it's time I show you how to unleash the full power of MongoDB queries. When repository methods aren't enough, the Criteria API and MongoTemplate can be very helpful.

    This article is part of our comprehensive 4-part MongoDB with Spring Boot series.

    Part 1: Getting Started with MongoDB: Installation and Basic Commands

    Part 2: MongoDB with Spring Boot using Spring-Data-MongoDB

    Part 3: MongoDB Relationships in Spring Boot

    Part 4 (Current): Advanced MongoDB Queries: Mastering Criteria API and MongoTemplate

    Repository methods are great for simple queries, but real-world applications often need:

    • Dynamic queries based on user input
    • Complex conditions with multiple optional parameters
    • Aggregations and data transformations
    • Bulk operations and updates

    This is where MongoTemplate and Criteria API shine. They give you the flexibility of MongoDB's native query language with the type safety of Java.

    Getting Started with MongoTemplate#

    MongoTemplate is the core class for interacting with MongoDB. It's automatically configured by Spring Boot:

    @Service public class OrderService { @Autowired private MongoTemplate mongoTemplate; public List<Order> findOrdersWithTemplate() { return mongoTemplate.findAll(Order.class); } }

    But that's just scratching the surface. Let's dive into the powerful features.

    The Criteria API: Building Dynamic Queries#

    Criteria API lets you build queries programmatically. Think of it as a query builder that adapts to your needs.

    Basic Criteria Queries#

    Let's start with simple criteria:

    @Test void basicCriteriaQuery() { Query query = new Query(); query.addCriteria(Criteria.where("status").is("pending")); List<Order> orders = mongoTemplate.find(query, Order.class); }

    Combining Multiple Criteria#

    Real queries often have multiple conditions:

    @Test void multipleCriteria() { Criteria criteria = Criteria.where("status").is("delivered") .and("totalPrice").gte(100) .and("quantity").lt(5); Query query = new Query(criteria); List<Order> orders = mongoTemplate.find(query, Order.class); }

    Using OR Conditions#

    Sometimes you need OR logic:

    @Test void orConditions() { Criteria criteria = new Criteria().orOperator( Criteria.where("status").is("pending"), Criteria.where("status").is("processing") ); Query query = new Query(criteria); List<Order> orders = mongoTemplate.find(query, Order.class); assertTrue(orders.stream() .allMatch(o -> o.getStatus().equals("pending") || o.getStatus().equals("processing")) ); }

    Complex Nested Conditions#

    For complex business logic, combine AND and OR:

    @Test void complexNestedQuery() { Criteria criteria = new Criteria().andOperator( Criteria.where("totalPrice").gte(50), new Criteria().orOperator( Criteria.where("status").is("pending"), Criteria.where("quantity").gt(10) ) ); Query query = new Query(criteria); List<Order> orders = mongoTemplate.find(query, Order.class); // Orders with price >= 50 AND (status = pending OR quantity > 10) }

    Dynamic Query Building#

    Here's where Criteria API really shines - building queries based on user input:

    @Service public class OrderSearchService { @Autowired private MongoTemplate mongoTemplate; public List<Order> searchOrders(OrderSearchCriteria searchCriteria) { Query query = new Query(); if (searchCriteria.getStatus() != null) { query.addCriteria(Criteria.where("status") .is(searchCriteria.getStatus())); } if (searchCriteria.getMinPrice() != null) { query.addCriteria(Criteria.where("totalPrice") .gte(searchCriteria.getMinPrice())); } if (searchCriteria.getMaxPrice() != null) { query.addCriteria(Criteria.where("totalPrice") .lte(searchCriteria.getMaxPrice())); } if (searchCriteria.getCity() != null) { query.addCriteria(Criteria.where("address.city") .is(searchCriteria.getCity())); } if (searchCriteria.getDateFrom() != null) { query.addCriteria(Criteria.where("createdAt") .gte(searchCriteria.getDateFrom())); } // Add sorting if (searchCriteria.getSortBy() != null) { Sort.Direction direction = searchCriteria.isAscending() ? Sort.Direction.ASC : Sort.Direction.DESC; query.with(Sort.by(direction, searchCriteria.getSortBy())); } // Add pagination if (searchCriteria.getPage() != null && searchCriteria.getSize() != null) { Pageable pageable = PageRequest.of( searchCriteria.getPage(), searchCriteria.getSize() ); query.with(pageable); } return mongoTemplate.find(query, Order.class); } }

    Querying Embedded Documents and References#

    Querying Embedded Documents#

    @Test void queryEmbeddedDocument() { // Find orders in specific cities Query query = new Query( Criteria.where("address.city").in("Mumbai", "Delhi", "Bangalore") ); List<Order> orders = mongoTemplate.find(query, Order.class); // Find orders with specific zip code pattern Query zipQuery = new Query( Criteria.where("address.zipCode").regex("^560.*") ); List<Order> bangaloreOrders = mongoTemplate.find(zipQuery, Order.class); }

    Handling DBRef Queries#

    Querying referenced documents requires special handling:

    @Test void queryWithDBRef() { // First, find the product Product product = mongoTemplate.findOne( Query.query(Criteria.where("name").is("Gaming Laptop")), Product.class ); // Then query orders containing this product Query query = new Query( Criteria.where("products.$id").is(new ObjectId(product.getId())) ); List<Order> orders = mongoTemplate.find(query, Order.class); }

    Text Search Capabilities#

    MongoDB supports full-text search. First, create a text index:

    @Document(collection = "products") @TextIndexed public class Product { @TextIndexed(weight = 2) private String name; @TextIndexed private String description; // other fields... }

    Then search using text criteria:

    @Test void textSearch() { TextCriteria criteria = TextCriteria .forDefaultLanguage() .matchingAny("wireless", "bluetooth"); Query query = TextQuery.queryText(criteria) .sortByScore(); List<Product> products = mongoTemplate.find(query, Product.class); }

    Update Operations with MongoTemplate#

    Single Document Updates#

    Update specific fields without fetching the document:

    @Test void updateSingleDocument() { Query query = new Query(Criteria.where("id").is(orderId)); Update update = new Update() .set("status", "shipped") .inc("quantity", 1) .currentDate("updatedAt"); UpdateResult result = mongoTemplate.updateFirst(query, update, Order.class); assertEquals(1, result.getModifiedCount()); }

    Bulk Updates#

    Update multiple documents at once:

    @Test void bulkUpdate() { Query query = new Query( Criteria.where("status").is("pending") .and("createdAt").lt(LocalDateTime.now().minusDays(7)) ); Update update = new Update() .set("status", "cancelled") .set("cancellationReason", "Order timeout"); UpdateResult result = mongoTemplate.updateMulti(query, update, Order.class); System.out.println("Updated " + result.getModifiedCount() + " orders"); }

    Upsert Operations#

    Create or update in one operation:

    @Test void upsertOperation() { Query query = new Query(Criteria.where("id").is("special-order-123")); Update update = new Update() .set("status", "vip") .setOnInsert("createdAt", LocalDateTime.now()) .inc("updateCount", 1); UpdateResult result = mongoTemplate.upsert(query, update, Order.class); if (result.getUpsertedId() != null) { System.out.println("Created new order"); } else { System.out.println("Updated existing order"); } }

    Projections and Field Selection#

    Optimize queries by fetching only needed fields:

    @Test void projectionExample() { Query query = new Query(); query.fields() .include("status") .include("totalPrice") .exclude("_id"); List<Order> orders = mongoTemplate.find(query, Order.class); // Only status and totalPrice are populated }

    For nested fields:

    @Test void nestedProjection() { Query query = new Query(); query.fields() .include("status") .include("address.city") .include("address.state"); List<Order> orders = mongoTemplate.find(query, Order.class); }

    Custom Queries with Native MongoDB#

    Sometimes you need the full power of native MongoDB queries:

    @Test void nativeMongoQuery() { String jsonQuery = "{ 'status': 'pending', " + "'$or': [ " + "{ 'totalPrice': { '$gte': 100 } }, " + "{ 'quantity': { '$gt': 5 } } " + "] }"; BasicQuery query = new BasicQuery(jsonQuery); List<Order> orders = mongoTemplate.find(query, Order.class); }

    Conclusion#

    Congratulations! You've completed my comprehensive MongoDB with Spring Boot series.

    You're no longer just a MongoDB user - you're a MongoDB developer capable of building sophisticated, performant applications. The document database that seemed foreign at first is now a powerful tool in your arsenal.

    Remember, MongoDB's flexibility is both its strength and responsibility. Design your documents thoughtfully, index strategically, and always consider how your application will query the data. With the skills you've gained, you're ready to tackle real-world projects that demand the scalability and flexibility of NoSQL databases.

    Happy coding!

    Want to Master Spring Boot and Land Your Dream Job?

    Struggling with coding interviews? Learn Data Structures & Algorithms (DSA) with our expert-led course. Build strong problem-solving skills, write optimized code, and crack top tech interviews with ease

    Learn more
    Spring Boot
    MongoDB
    MongoTemplate
    Criteria API

    Subscribe to our newsletter

    Read articles from Coding Shuttle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

    More articles