Sometimes you need to create a new collection on the fly from a MongoDB aggregation pipeline. Fortunately, you can easily do that with MongoTemplate using the addToSet operation.

And in this guide, I'll show you how to turn that dream into a reality.

If you prefer, though, you can go straight to the code.

Or just watch me deliver with this guide.

The Business Requirements

Your boss Smithers walks into your office. He's got some bounce in his step since he won that award for "Most Likely to Play an Extra in a James Bond Movie."

"I got a new requirement for ya," he says as he sits down. "Regarding that CRM app: I need you to produce a service request that shows which lines of business each contact is generating sales for."

He smiles. Still happy about that award.

"No sales figures needed," he says. "We just need the LOBs."

He stands up and skips out of your office.

Building On

Fortunately, you already know how to use MongoDB aggregations to produce that kind of report within a Spring Boot application.

And if you got here from the Google and you don't know how to do that, feel free to start at square one with the guide on using MongoTemplate with aggregations. Then we'll see you back here.

Now you know based on the requirements that you'll use the group aggregation operation to group all sales by contact.

But what about the line of business thing? What are you going to do there?

That's where the addToSet operation comes in handy. It lets you create new arrays that you can add to your output documents.

Even better: it doesn't add dupes. So if a single contact has more than one sale from a specific line of business, that line of business will only show up in the array once

Isn't it great when someone else does all the hard work for you?

Now let's get busy.

Simple Addition

Head over to your ContactService class and add a new method that looks like this:

	public List<SaleInfo> findLobSalesByContact() {
	    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").addToSet("sales.lineOfBusiness").as("lobs");
	    AggregationOperation project = Aggregation.project("lobs").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;
	}

There are four (4) stages in that aggregation pipeline. I'll cover them in detail.

The first stage (unwind) lives up to its name. It unwinds the collection of contacts around their respective sales. If you don't know what that means, feel free to check out my guide on unwinding with MongoDB aggregations.

The second stage (fullName) creates a new field that includes the contact's first and last name. It's an easier way of grouping the contacts.

The third stage (group) groups the sales by contact. But then it does something else.

That stage handles the addToSet operation as well. Specifically, it's adding the line of business ("sales.lineOfBusiness") for each sale to a new array called "lobs." That new array name is specified by the as() method.

The final stage (project) associates the contact's full name with the line of business array.

Also, the final result returns an array of SalesInfo items. Be sure you code that inner class correctly:

    public static class SaleInfo {
        private String contactName;
        private List<String> lobs = new ArrayList<String>();		
        
        public String getContactName() {
        	return contactName;
        }
        public void setContactName(String contactName) {
        	this.contactName = contactName;
        }
        public List<String> getLobs() {
            return lobs;
        }
        public void setLobs(List<String> lobs) {
            this.lobs = lobs;
        }
    }

And that's it.

That aggregation pipeline, by the way, will look like this if you run it from your MongoDB client:

db.contacts.aggregate(
[
    {
        "$unwind": {
            "path": "$sales",
            "preserveNullAndEmptyArrays": true
        }
    },
    {
        "$project": {
            "_id": 1,
            "sales": 1,
            "contactName": {
                "$concat": [
                    "$firstName",
                    " ",
                    "$lastName"
                ]
            }
        }
    },
    {
        "$group": {
            "_id": "$contactName",
            "lobs": {
                "$addToSet": "$sales.lineOfBusiness"
            }
        }
    },
    {
        "$project": {
            "lobs": 1,
            "_id": 0,
            "contactName": "$_id"
        }
    }
]
)

Testing Time

Before you test, 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"
  } ]
} ]

Now create some initialization code that will run the aggregation pipeline once you launch the Spring Boot application. Make it look like this:

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

As you can see, that's going to get the results from MongoDB and print them out in a nice shade of red in JSON format.

So save that file and restart Spring Boot. Once it's finished loading, you should see this output:

[ {
  "contactName" : "Governor Tarkin",
  "lobs" : [ "FULL_STACK", "JAVA_ENTERPRISE" ]
}, {
  "contactName" : "Lando Calrissian",
  "lobs" : [ "DEV_OPS" ]
}, {
  "contactName" : "Han Solo",
  "lobs" : [ ]
}, {
  "contactName" : "Chew Bacca",
  "lobs" : [ "ANGULAR" ]
}, {
  "contactName" : "JarJar Binks",
  "lobs" : [ "FULL_STACK", "ANGULAR" ]
}, {
  "contactName" : "Luke Skywalker",
  "lobs" : [ ]
}, {
  "contactName" : "Boba Fett",
  "lobs" : [ "DEV_OPS" ]
}, {
  "contactName" : "R2D2 Droid",
  "lobs" : [ ]
}, {
  "contactName" : "Jabba Hutt",
  "lobs" : [ "ANGULAR" ]
}, {
  "contactName" : "Princess Leia",
  "lobs" : [ "ANGULAR", "JAVA_ENTERPRISE" ]
} ]

Bingo! That's exactly what you're looking for.

Now management can see which contacts are generating sales for specific lines of business.

Wrapping It Up

Excellent. You now know how to do what you came here to learn how to do.

Why don't you use addToSet for something else? Maybe you'd like to get all sales amounts instead of LOBs per contact.

And be sure to view the source code on GitHub if you need to.

Have fun!

Photo by Sharon McCutcheon on Unsplash