Did you know that you can put conditionals in your MongoDB aggregation pipelines? Even better: you can do so within your Spring Boot application using MongoTemplate.

In this guide, I'll show you how to do just that.

Alternatively, you can just grab the source code on GitHub.

The Business Requirements

Your boss Smithers walks into your office. He's got a toothpick hanging out of his mouth.

"About that CRM app you're working on," he says while trying not to lose the toothpick. "I need you to make a change."

He moves the toothpick from the left side of his mouth to the right side. Almost magically.

"Management wants to know all the contacts that are bringing in the biggest sales. Put together a service request that shows the number of sales with a value of at least $2,000 for each contact.:"

He moves the toothpick back to the left side of his mouth as he walks out of your office.

The Service, Continued

You've already created a ContactService class that handles multiple MongoDB aggregation pipelines. So this should be fairly easy.

However, if you just landed here from the Google and you're not clear on how to use MongoTemplate with Spring Boot, feel free to read my initial guide on this subject. That will help.

And then come back here.

Now let's assume that the data in your contacts database looks like this:

[ {
  "id" : "5fde12d60ab013769b67cf02",
  "firstName" : "Chew",
  "lastName" : "Bacca",
  "email" : "chewie@xmail.com",
  "source" : "WEBSITE_FORM",
  "status" : "ACTIVE",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR" ],
  "company" : "Working for Han",
  "title" : "Wookie",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  },
  "sales" : [ {
    "purchaseOrderNumber" : "09393",
    "title" : "Angular UI Refactoring",
    "value" : 100000,
    "date" : 1609650000000,
    "lineOfBusiness" : "ANGULAR"
  }, {
    "purchaseOrderNumber" : "09394",
    "title" : "Revamp CRM UI",
    "value" : 150000,
    "date" : 1610254800000,
    "lineOfBusiness" : "ANGULAR"
  }, {
    "purchaseOrderNumber" : "09395",
    "title" : "Angular tutorial",
    "value" : 20000,
    "date" : 1610427600000,
    "lineOfBusiness" : "ANGULAR"
  } ]
}, {
  "id" : "5fde1028792009283c603929",
  "firstName" : "JarJar",
  "lastName" : "Binks",
  "email" : "jarjar@xmail.com",
  "addresses" : [ {
    "street1" : "1400 Plum Way",
    "city" : "Onisius",
    "state" : "NM",
    "zip" : "80909",
    "addressType" : "HOME"
  } ],
  "source" : "WALKIN",
  "status" : "CONTACTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "FULL_STACK" ],
  "company" : "None",
  "title" : "Comic Relief",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  },
  "sales" : [ {
    "purchaseOrderNumber" : "09396",
    "title" : "Spring Boot API for In-House REST Service",
    "value" : 310000,
    "date" : 1609736400000
  }, {
    "purchaseOrderNumber" : "09396",
    "title" : "Angular Optimization",
    "value" : 200000,
    "date" : 1610859600000,
    "lineOfBusiness" : "ANGULAR"
  }, {
    "purchaseOrderNumber" : "09397",
    "title" : "Full Stack Work",
    "value" : 110000,
    "date" : 1610946000000,
    "lineOfBusiness" : "FULL_STACK"
  } ]
}, {
  "id" : "5fde117edd79e20e3ff6528c",
  "firstName" : "Lando",
  "lastName" : "Calrissian",
  "email" : "lando@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "WORK",
    "countryCode" : "us"
  } ],
  "source" : "INBOUND_SALES_CALL",
  "status" : "CONTACTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR", "DEV_OPS" ],
  "company" : "Cloud City",
  "title" : "Friend",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  },
  "sales" : [ {
    "purchaseOrderNumber" : "09398",
    "title" : "Harness Training",
    "value" : 150000,
    "date" : 1610082000000,
    "lineOfBusiness" : "DEV_OPS"
  }, {
    "purchaseOrderNumber" : "09399",
    "title" : "Jenkins Training",
    "value" : 150000,
    "date" : 1610254800000,
    "lineOfBusiness" : "DEV_OPS"
  } ]
}, {
  "id" : "5fde1ac084dad94dbb7f82ae",
  "firstName" : "R2D2",
  "lastName" : "Droid",
  "email" : "r2d2@xmail.com",
  "source" : "EMAIL",
  "status" : "ACTIVE",
  "statusChange" : 0,
  "linesOfBusiness" : [ "JAVA_ENTERPRISE" ],
  "company" : "For Luke",
  "title" : "Droid",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fdd0af34e9d6806f369abf0",
  "firstName" : "Boba",
  "lastName" : "Fett",
  "email" : "boba@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "HOME",
    "countryCode" : "us"
  } ],
  "addresses" : [ {
    "street1" : "1222 Galaxy Way",
    "city" : "Alterion",
    "state" : "AR",
    "zip" : "22222",
    "country" : "US",
    "addressType" : "HOME"
  } ],
  "source" : "INBOUND_SALES_CALL",
  "status" : "CONTACTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "DEV_OPS" ],
  "company" : "Empire",
  "title" : "Bounty Hunter",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  },
  "sales" : [ {
    "purchaseOrderNumber" : "09400",
    "title" : "Jenkins Training",
    "value" : 150000,
    "date" : 1610341200000,
    "lineOfBusiness" : "DEV_OPS"
  }, {
    "purchaseOrderNumber" : "09401",
    "title" : "Kubernetes Training",
    "value" : 200000,
    "date" : 1610686800000,
    "lineOfBusiness" : "DEV_OPS"
  } ]
}, {
  "id" : "5fdd0cedaac5f75d62564ee7",
  "firstName" : "Jabba",
  "lastName" : "Hutt",
  "email" : "jabba@xmail.com",
  "source" : "EMAIL",
  "status" : "NEW",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR" ],
  "company" : "Sandz",
  "title" : "Large",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  },
  "sales" : [ {
    "purchaseOrderNumber" : "09403",
    "title" : "Update UI to Match Wireframes",
    "value" : 200000,
    "date" : 1609822800000,
    "lineOfBusiness" : "ANGULAR"
  }, {
    "purchaseOrderNumber" : "09403",
    "title" : "Improve UI Responsiveness",
    "value" : 150000,
    "date" : 1610254800000,
    "lineOfBusiness" : "ANGULAR"
  } ]
}, {
  "id" : "5fdd0e7c870ef4713e179384",
  "firstName" : "Princess",
  "lastName" : "Leia",
  "email" : "leia@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "WORK",
    "countryCode" : "us"
  } ],
  "source" : "WALKIN",
  "status" : "INTERESTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "JAVA_ENTERPRISE", "ANGULAR" ],
  "company" : "Republic",
  "title" : "Princess",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  },
  "sales" : [ {
    "purchaseOrderNumber" : "09404",
    "title" : "Upgrade Spring Boot application to Java 11",
    "value" : 150000,
    "date" : 1610341200000,
    "lineOfBusiness" : "JAVA_ENTERPRISE"
  }, {
    "purchaseOrderNumber" : "09406",
    "title" : "Improve Exception Handling in Spring Boot application",
    "value" : 250000,
    "date" : 1610600400000,
    "lineOfBusiness" : "JAVA_ENTERPRISE"
  }, {
    "purchaseOrderNumber" : "09407",
    "title" : "Support Additional External REST services in Spring Boot",
    "value" : 350000,
    "date" : 1611118800000,
    "lineOfBusiness" : "JAVA_ENTERPRISE"
  }, {
    "purchaseOrderNumber" : "09408",
    "title" : "Enhance UI for Accounting App",
    "value" : 150000,
    "date" : 1611205200000,
    "lineOfBusiness" : "ANGULAR"
  } ]
}, {
  "id" : "5fd5f37bde602d3bacef69db",
  "firstName" : "Luke",
  "lastName" : "Skywalker",
  "email" : "luke@tat2.com",
  "source" : "EMAIL",
  "sourceDetails" : "He emailed me",
  "status" : "NEW",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR" ],
  "company" : "International Business",
  "title" : "President",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fdd0f2aea599836ca3ddbf1",
  "firstName" : "Han",
  "lastName" : "Solo",
  "email" : "han@xmail.com",
  "addresses" : [ {
    "street1" : "111 Millennium Way",
    "city" : "Nessy",
    "state" : "CO",
    "addressType" : "HOME"
  } ],
  "source" : "EMAIL",
  "status" : "ACTIVE",
  "statusChange" : 0,
  "title" : "Pirate",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "6005e87163660b7b2e6a16df",
  "firstName" : "Governor",
  "lastName" : "Tarkin",
  "email" : "tarkin@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "CELL"
  } ],
  "addresses" : [ {
    "city" : "Home City",
    "state" : "MN",
    "addressType" : "HOME"
  } ],
  "source" : "EMAIL",
  "status" : "ACTIVE",
  "linesOfBusiness" : [ "FULL_STACK" ],
  "company" : "No Moon",
  "title" : "Governor",
  "authority" : true,
  "salesOwner" : {
    "id" : "6005e76bac127e0f5d9a6560",
    "firstName" : "The",
    "lastName" : "Emperor",
    "email" : "theemperor@xmail.com",
    "username" : "theemperor",
    "phoneNumber" : "(555) 555-5555"
  },
  "sales" : [ {
    "purchaseOrderNumber" : "09409",
    "title" : "Refactor Spring Boot application",
    "value" : 150000,
    "date" : 1610341200000,
    "lineOfBusiness" : "JAVA_ENTERPRISE"
  }, {
    "purchaseOrderNumber" : "09410",
    "title" : "Full stack refactoring",
    "value" : 250000,
    "date" : 1610600400000,
    "lineOfBusiness" : "FULL_STACK"
  } ]
} ]

As you can see, some contacts have sales. Others don't.

Remember: the requirement here is to just add up the number of individual sales that equal or exceed $2,000.

Also remember: the "value" field here is in cents, not dollars. So a value of 200,000 or more is what you're looking for.

With that in mind. It's time to give Smithers what he wants.

Conditional Love

Update that ContactService class to add a new method. Make it look like this:

	public List<SaleInfo> getBigSalesPerContact() {
		ConditionalOperators.Cond bigSalesCount = ConditionalOperators
				.when(new Criteria("sales.value")
				.gte(200000))
				.then(1)
				.otherwise(0);
		
		AggregationOperation unwind = Aggregation.unwind("sales", true);
		AggregationOperation fullName = Aggregation.project("_id", "sales").and("firstName").concat(" ", Aggregation.fields("lastName")).as("contactName");
		AggregationOperation group = Aggregation.group("contactName").sum(bigSalesCount).as("bigSales");
		AggregationOperation project = Aggregation.project("bigSales").and("contactName").previousOperation();
		
		Aggregation aggregation = Aggregation.newAggregation(unwind, fullName, group, project);

		List<SaleInfo> saleInfo = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Contact.class), SaleInfo.class).getMappedResults();
		
		return saleInfo;
	}

If you've been following along here on these aggregate operations, you'll notice that the code above looks a little different from what you've seen in the past.

For starters, it uses ConditionalOperators.Cond. What the heck is that?

Well, that's why your here. To learn about that.

It encapsulates the $cond operator that you'd use if you wanted to handle this same aggregation pipeline on your MongoDB client.

It's the class that lets you deal with if...then scenarios. Which is what you'll do here.

Keep in mind: ConditionalOperators.Cond implements AggregationExpression. So you can use it wherever you use AggregationExpression.

You'll use it in sum() here.

In the code above, the conditional asks the following question: is this sales value greater than or equal to 200,000?

If the answer is yes, then it returns a value of 1. Otherwise, it returns a value of 0.

You can think of it as substituting the sales value for either 1 or 0, depending on the amount of the sales value.

Ultimately, the aggregation pipeline will add up the numbers (1's or 0's) and that summation will equal the number of sales greater than or equal to $2,000.

Now, take a look at the pipeline itself.

The first stage in the pipeline (unwind) unwinds the contacts around the sales. I've already explained unwind in a previous guide.

But behold! This one is slightly different than what I've done previously. Here, the unwind() method uses two (2) parameters.

The first parameter is the field to unwind. The second parameter is a boolean.

You're probably already familiar with the first parameter. So what's the second parameter?

It's set to true here because you want to unwind contacts that don't have any sales. That way you can still give a report about them. Of course, their number of sales exceeding $2,000 will be 0 because they have no sales at all.

I hadn't recommended that in previous guides. But I thought the time was right to do it here.

The second stage (fullName) creates the full contact name from the contact's first and last name.

The third stage (group) groups the sales by contact. But once again, there's something a little different about this grouping.

Look closely and you'll see it's using the conditional you created above in the sum() method. That's where it's going to sum up all the sales that are equal to or greater than $2,000. It saves that summation as a new field called "bigSales."

The last stage (project) associates the contact's full name with that "bigSales" field from the previous stage.

And once again you'll hijack the SalesInfo inner class to make this thing work. Update it to look like this:

	public static class SaleInfo {
		private String contactName;
		private Sale sale;
		private Integer totalSales;
		private Integer bigSales;
		
		
		public String getContactName() {
			return contactName;
		}
		public void setContactName(String contactName) {
			this.contactName = contactName;
		}
		public Sale getSale() {
			return sale;
		}
		public void setSale(Sale sale) {
			this.sale = sale;
		}
		public Integer getTotalSales() {
			return totalSales;
		}
		public void setTotalSales(Integer totalSales) {
			this.totalSales = totalSales;
		}
		public Integer getBigSales() {
			return bigSales;
		}
		public void setBigSales(Integer bigSales) {
			this.bigSales = bigSales;
		}
	}

The sale and totalSales properties won't be used here.

And now for the obligatory "here's what this aggregation pipeline would look like if you run it on your MongoDB client" moment:

db.contacts.aggregate(
[
    {
        "$unwind": {
            "path": "$sales",
            "preserveNullAndEmptyArrays": true
        }
    },
    {
        "$project": {
            "_id": 1,
            "sales": 1,
            "contactName": {
                "$concat": [
                    "$firstName",
                    " ",
                    "$lastName"
                ]
            }
        }
    },
    {
        "$group": {
            "_id": "$contactName",
            "bigSales": {
                "$sum": {
                    "$cond": {
                        "if": {
                            "$gte": [
                                "$sales.value",
                                200000
                            ]
                        },
                        "then": 1,
                        "else": 0
                    }
                }
            }
        }
    },
    {
        "$project": {
            "bigSales": 1,
            "_id": 0,
            "contactName": "$_id"
        }
    }
]
)

Test! Test!! Test!!!

Now it's time to test this whole thing out.

As is usually the case, I recommend using initialization code to test it out by simply launching your Spring Boot application. Go to now:

@Component
public class ApplicationListenerInitialize implements ApplicationListener<ApplicationReadyEvent>  {
	
	@Autowired
	private ContactService contactService;
	 
    public void onApplicationEvent(ApplicationReadyEvent event) {        	
    	List<SaleInfo> sales = contactService.getBigSalesPerContact();
    	
    	try {
	    	ObjectMapper objectMapper = new ObjectMapper();
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
	    	System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(sales));
    	} catch (Exception e) {
    		e.printStackTrace();
    	}
    }
}

Save that and launch Spring Boot. You should get something like this in a nice shade of red:

[ {
  "contactName" : "Governor Tarkin",
  "bigSales" : 1
}, {
  "contactName" : "Lando Calrissian",
  "bigSales" : 0
}, {
  "contactName" : "Han Solo",
  "bigSales" : 0
}, {
  "contactName" : "Chew Bacca",
  "bigSales" : 0
}, {
  "contactName" : "JarJar Binks",
  "bigSales" : 2
}, {
  "contactName" : "Luke Skywalker",
  "bigSales" : 0
}, {
  "contactName" : "Boba Fett",
  "bigSales" : 1
}, {
  "contactName" : "R2D2 Droid",
  "bigSales" : 0
}, {
  "contactName" : "Jabba Hutt",
  "bigSales" : 1
}, {
  "contactName" : "Princess Leia",
  "bigSales" : 2
} ]

Now if you go through the list above and do the number-crunching manually, you'll see that it all adds up.

Wrapping It Up

Congratulations! You now know how to use conditionals in your aggregation pipelines with MongoTemplate.

Now try something harder. Make a more complicated conditional. Use compound conditionals. Do something other than math.

There's a world of possibilities out there.

And remember, feel free to grab the code on GitHub.

Have fun!