Sometimes you just need the first element in a group you retrieved with a MongoDB aggregation. Fortunately, that's easy to do with MongoTemplate in Spring Boot.

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

If you'd rather just go straight to the source code, you can do that as well.

Otherwise, hang around here and see how it's done.

The Business Requirements

Your boss Smithers walks into your office. He seems a bit high on life after winning that chess championship match.

"I got a new requirement for ya!" he says with a smile. "I need ya to create a service request in the CRM app that gets info about just the first sale for each contact."

He pauses and smiles again.

"Can you do that?"

Before you can answer he leaves your office.

Still Not Starting Over

By this point, you've already done quite a bit of work when it comes to retrieving info from the contacts in your database. So this shouldn't be that challenging.

However, if you're brand new to Spring Boot with MongoTemplate, and aggregations, feel free to read my guide on how to get started with aggregation pipelines. I hope you enjoy reading it as much as I enjoyed writing it.

Once you're done there, come back here.

Now to focus on where you're at right now. Let's assume that the contacts in your MongoDB contacts collection look 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"
  } ]
} ]

So take a close look at that and you'll see that most of the contacts have sales.

Pay close attention to the "sales" field. It's an array of documents that each mimic the Sale class on the Java side.

That info is important because Smithers wants you to create a service request that retrieves the first sale for each contact. 

Now let's get started.

The First of Your Name

As a reminder: the whole point of this article is to describe how to use the first operation with the group pipeline operation. So you'll use that to fulfill the requirement that Smithers laid out.

And here's how you do that. Create a new method in ContactService that looks like this:

	public List<SaleInfo> findFirstSaleForEachContact() {
		AggregationOperation match = Aggregation.match(Criteria.where("sales").exists(true));
		AggregationOperation unwind = Aggregation.unwind("sales");
		AggregationOperation sort = Aggregation.sort(Direction.ASC, "sales.date");
		AggregationOperation fullName = Aggregation.project("_id", "sales").and("firstName").concat(" ", Aggregation.fields("lastName")).as("contactName");
		AggregationOperation group = Aggregation.group("contactName").first("sales").as("sale");
		AggregationOperation project = Aggregation.project("sale").and("contactName").previousOperation();
		
		Aggregation aggregation = Aggregation.newAggregation(match, unwind, sort, fullName, group, project);

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

A lot of stages in that pipeline. I'll break them down one by one.

The first stage (match) eliminates any contacts that don't have sales. Easy enough.

The second stage (unwind) handles the unwinding. I've already covered the subject of unwinding in a separate guide. Feel free to check it out if you're unsure how it works.

The thrid stage (sort) sorts the sales by date in ascending order. That's necessary because you need to find the first sale for each contact. That will be the sale with the earliest date.

And let me pause right here and say that using first with group in a MongoDB aggregation really doesn't make any sense unless you've first sorted the documents. Otherwise, what are you getting the first of?

The fourth stage (fullName) creates a new field called "contactName." It's a concatenation of the contact's first and last name with a space in between. It's the field you'll use to group the sales.

Speaking of grouping sales, that's handled in the fifth stage (group). But that stage also uses the operation that brought you here: first.

In this case, the first operation will grab the first sale from the list. And since the list is sorted by date in ascending order, it will grab the earliest sale. That's what you want.

That fifth stage also creates a new field called "sale" that represents the first sale in the group. It does that with the as() method.

That brings us to the sixth stage (project). It marries the sale with the contact's full name so it returns the two together.

Now take a look at the mongoTemplate.aggregate() method above. You'll see that it's using something called SalesInfo.class and you haven't created that class yet.

So you'd better do so now. Make it an inner class to keep things tidy:

	public static class SaleInfo {
		private String contactName;
		private Sale sale;

		
		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;
		}
	}

That class does the same thing that the last stage in the pipeline does: it associates a contact's full name with a specific sale.

In this case, that specific sale will be the first sale for the contact.

By the way, if you want to know what that aggregation pipeline looks like on the MongoDB side of the house, here it is:

db.contacts.aggregate(
[
   {
      "$match":{
         "sales":{
            "$exists":true
         }
      }
   },
   {
      "$unwind":"$sales"
   },
   {
      "$sort":{
         "sales.date":1
      }
   },
   {
      "$project":{
         "_id":1,
         "sales":1,
         "contactName":{
            "$concat":[
               "$firstName",
               " ",
               "$lastName"
            ]
         }
      }
   },
   {
      "$group":{
         "_id":"$contactName",
         "sale":{
            "$first":"$sales"
         }
      }
   },
   {
      "$project":{
         "sale":1,
         "_id":0,
         "contactName":"$_id"
      }
   }
]
)

Trial With No Error (Hopefully)

Now it's time to test this thing out. And I like to do that with some initialization code. Make it look like this:

@Component
public class ApplicationListenerInitialize implements ApplicationListener<ApplicationReadyEvent>  {
	
	@Autowired
	private ContactService contactService;
	
    public void onApplicationEvent(ApplicationReadyEvent event) {        	
    	List<SaleInfo> sales = contactService.findFirstSaleForEachContact();
    	
    	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();
    	}
    }
}

Now restart your Spring Boot application. When it's loaded, it will execute the code above.

And you should see this output in red:

[ {
  "contactName" : "Governor Tarkin",
  "sale" : {
    "purchaseOrderNumber" : "09409",
    "title" : "Refactor Spring Boot application",
    "value" : 150000,
    "date" : 1610341200000,
    "lineOfBusiness" : "JAVA_ENTERPRISE"
  }
}, {
  "contactName" : "Princess Leia",
  "sale" : {
    "purchaseOrderNumber" : "09404",
    "title" : "Upgrade Spring Boot application to Java 11",
    "value" : 150000,
    "date" : 1610341200000,
    "lineOfBusiness" : "JAVA_ENTERPRISE"
  }
}, {
  "contactName" : "Lando Calrissian",
  "sale" : {
    "purchaseOrderNumber" : "09398",
    "title" : "Harness Training",
    "value" : 150000,
    "date" : 1610082000000,
    "lineOfBusiness" : "DEV_OPS"
  }
}, {
  "contactName" : "Boba Fett",
  "sale" : {
    "purchaseOrderNumber" : "09400",
    "title" : "Jenkins Training",
    "value" : 150000,
    "date" : 1610341200000,
    "lineOfBusiness" : "DEV_OPS"
  }
}, {
  "contactName" : "Chew Bacca",
  "sale" : {
    "purchaseOrderNumber" : "09393",
    "title" : "Angular UI Refactoring",
    "value" : 100000,
    "date" : 1609650000000,
    "lineOfBusiness" : "ANGULAR"
  }
}, {
  "contactName" : "JarJar Binks",
  "sale" : {
    "purchaseOrderNumber" : "09396",
    "title" : "Spring Boot API for In-House REST Service",
    "value" : 310000,
    "date" : 1609736400000
  }
}, {
  "contactName" : "Jabba Hutt",
  "sale" : {
    "purchaseOrderNumber" : "09403",
    "title" : "Update UI to Match Wireframes",
    "value" : 200000,
    "date" : 1609822800000,
    "lineOfBusiness" : "ANGULAR"
  }
} ]

And there you go. The first sale for each contact!

A couple of things to keep in mind here: the "value" property represents the sales value of the deal. However, it's in cents. I like doing it that way as opposed to messing around with decimal points in the back end.

Also, the date is a long value. It's the the same thing you'd see if you invoked getTime() on a Java Date object.

Wrapping It Up

You might think something is wrong with the output above because it's not in ascending order of sales dates. But that's okay.

Why? Because the sales were ordered by date per contact. So one contact's first order might be later than another contact's first order. That won't be reflected in the output above.

However, you can change that by adding one more simple stage. That will be your homework assignment.

Have fun!

Photo by Ketut Subiyanto from Pexels