Sometimes you need to take a whole bunch of data and convert it to just one number. You can do that easily with the reduce() method on a Java Stream object.

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

Even better: I'll show you how to do it with a dataset that's similar to what you might find in a real-world CRM application. 

So let's get started.

The CRM App

Let's say you're building a CRM app. It does the normal stuff that CRM apps do: tracks activities between sales reps and contacts.

Those activities get stored in a MongoDB database as documents. Each document persists info like the title of the activity as well as the type, outcome, location, start time, end time, notes, and the contact involved.

As it stands right now, if you retrieve all the documents from the activities collection, the resulting data set looks like the JSON dump at this link.

As you can see, we're not messing around here. You're going to be working with real-world data.

On the Java side, the Activity class with its related classes mimic the data set that you see above. You can see examples of those classes over on GitHub.

So you can just do a findAll() on that collection above and get a List of Java objects that represent that JSON output. Then, you can use a Java Stream to filter, find, and map as you see fit.

That's what you'll do in this guide.

Lead Scoring

Today it's all about lead scoring.

The sales reps who use your CRM app want to find the hottest leads. That way, they don't waste time with tire-kickers.

But how do they find those hot leads? With lead scoring.

Lead scoring ranks contacts on a scale of 0-100, with 100 being the hottest. 

The application you're working on scores contacts based on interest level. It determines interest level by sales activity outcomes.

If the contact showed an interest after a phone call, for example, then the application will increase the contact's score.

However, if the contact flat-out said that he or she wasn't interested, then that will decrease the lead score.

For this requirement, you'll fetch the activities of a specific contact the use any outcomes associated with the activities to generate a lead score.

Am I Guilty of Clickbait?

Although I'll show you how to handle that requirement I just outlined with reduce(), you really don't need to use it.

In fact, the older I get, the less of a need I see for reduce().

Why? Because the Stream API offers several convenience methods that handle reductions for you under the covers.

For example, you could handle the requirement above with sum() instead of reduce().

And it's easier to use sum().

So before you go hog-wild with reduce(), be sure to check out the Stream API docs to make sure that there isn't an easier way to do what you're trying to do.

But, presumably, you're here to learn about how to use reduce() so let's tackle this requirement that way.

Don't Forget the Function

To make this work, you'll need to start by instantiating a Function. That Function will accept an ActivityOutcome object as input and return an Integer as output.

The Integer output is the score value associated with that kind of outcome.

Here's what the whole thing looks like:

final class LeadScorer implements ToIntFunction<ActivityOutcome> {
    @Override
    public int applyAsInt(ActivityOutcome outcome) {
        String outcomeName = outcome.getName();
        Integer score = 0;
        
        if ("Interested".equals(outcomeName)) {
            score = 10;
        } else if ("Appointment Scheduled".equals(outcomeName)) {
            score = 5;
        } else if ("Did Not Respond".equals(outcomeName)) {
            score = -1;
        }
        
        return score;        
    }
}

As you can see, implementing that Function would be a little weird with a lambda expression. So it's a good idea to go "old school" here and implement the interface with a custom class.

The class examines the name of the outcome. Then, it assigns a score value based on the name.

  • Interested = 10 points
  • Appointment Scheduled = 5 points
  • Did Not Respond = -1 point

Keep in mind: a contact could have several outcomes. So the points for each outcome will need to be tallied.

That final sum is the lead score.

Now, that's a fairly simplistic way to handle lead scoring. I'll create a more sophisticated algorithm at some point in the future. 

For now, though, this works.

Your Dreams Are My Streams

Unsurprisingly, you'll use a Java Stream to handle calculating the final lead score.

The idea is to take all the activities for a contact, run the outcome for each activity through the Function you just created, and get a sum of all the scores.

Pretty straightforward.

First, though, make sure that you're getting the right answer. Just grab all the activities for the contact named Joy Yeezu and print them out. Then, do the math for the lead score in your head.

Start by running this code:

String contactId = "6014199147692f2a4194ff9a";

List<Activity> activities = activityRepo.findByContactId(contactId);

List<Activity> acts = activities
           .stream()
           .filter(activity -> activity.getOutcome() != null)
           .collect(Collectors.toList());
try {
    ObjectMapper objectMapper = new ObjectMapper();
    System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(acts));
} catch (Exception e) {
    e.printStackTrace();
}

That will print out all the activities with non-null outcomes for Joy Yeezu, whose contact ID happens to be specified in the first line of code there.

The output will look like this:

[ {
  "id" : "601fd76fda249c7531fd28f3",
  "type" : {
    "id" : "6016a64c33ffe20d90fa8232",
    "name" : "Email",
    "icon" : "email"
  },
  "title" : "Emailed me asking about REST development",
  "outcome" : {
    "id" : "6016a408ae0e817a115fd06c",
    "name" : "Appointment Scheduled"
  },
  "notes" : "She's looking to a legacy project into a solution with REST and MongoDB.",
  "location" : null,
  "startDate" : 1609855200000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fdafc8fc6df7eddcf3ae2",
  "type" : {
    "id" : "6016a960ca8c08019b4dfcbd",
    "name" : "Chat",
    "icon" : "chat"
  },
  "title" : "Chatted with her via Skype",
  "outcome" : {
    "id" : "6016a408ae0e817a115fd06a",
    "name" : "Interested"
  },
  "notes" : "Talked about our history of delivering outstanding Java Enterprise software.",
  "location" : null,
  "startDate" : 1610367300000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fdc208fc6df7eddcf3ae4",
  "type" : {
    "id" : "6016a64c33ffe20d90fa8232",
    "name" : "Email",
    "icon" : "email"
  },
  "title" : "Email Followup from lunch",
  "outcome" : {
    "id" : "6016a407ae0e817a115fd069",
    "name" : "Did Not Respond"
  },
  "notes" : null,
  "location" : null,
  "startDate" : 1610627400000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
}, {
  "id" : "601fde938fc6df7eddcf3ae5",
  "type" : {
    "id" : "6016a64c33ffe20d90fa8232",
    "name" : "Email",
    "icon" : "email"
  },
  "title" : "Sent another email just to follow up",
  "outcome" : {
    "id" : "6016a407ae0e817a115fd069",
    "name" : "Did Not Respond"
  },
  "notes" : "This was a follow-up to the first email.",
  "location" : null,
  "startDate" : 1610713800000,
  "endDate" : null,
  "contact" : {
    "id" : "6014199147692f2a4194ff9a",
    "firstName" : "Yeezu",
    "lastName" : "Joy",
    "account" : {
      "id" : "60141483b56f731fe77e9031",
      "name" : "Empire"
    },
    "salesOwner" : {
      "id" : "6014081e221e1b534a8aa432",
      "firstName" : "Milton",
      "lastName" : "Jones",
      "username" : "milton"
    }
  }
} ]

Now if you go through that list, you'll find that Joy has two activities that resulted in "Did Not Respond," one activity that resulted in "Interested," and one activity that resulted in "Appointment Scheduled."

That means she should have a lead score of 13 (10 + 5 - 1 - 1 = 13).

Now let's put together some code that calculates the lead score. It should look like this:

String contactId = "6014199147692f2a4194ff9a";

List<Activity> activities = activityRepo.findByContactId(contactId);

LeadScorer leadScorer = new LeadScorer();

int score = activities
           .stream()
           .filter(activity -> activity.getOutcome() != null)
           .map(activity -> activity.getOutcome())
           .mapToInt(leadScorer)
           .reduce(0, (accumulatedScore, currentScore) -> currentScore + accumulatedScore);

System.err.println(score);

That code grabs all activities for Joy Yeezu based on her contact ID. They get returned as a List of Activity objects.

Then, it instantiates the LeadScorer class you created in the previous section.

Next, the code translates the List of Activity objects into a Stream. That's handled with the stream() method.

After that, the code filters out all activities with null outcomes. That's in the first filter() method.

If you'd like to learn more about how that works, feel free to look at my guide on filters in Streams.

Next, the code uses the map() method to translate each Activity object into its ActivityOutcome object.

At this point, the Stream sequence includes only ActivityOutome objects.

The mapToInt() method translates the ActivityOutcome objects to integers. It does that using the LeadScorer class you created earlier.

Those integers, by the way, represent the score for each activity outcome.

Now all that's left to do is to get a sum of all the integers in the sequence. As I said earlier, you can stop here with sum().

But the code above accomplishes the same goal with reduce().

The first parameter in the reduce() method is the identity. That's the starting point or the default value.

In this case, the identity is 0 because it starts with a lead score of 0.

The next parameter in reduce() is the accumulator, expressed as a BinaryOperator.

That accumulator accepts two inputs. The first input is the result of the accumulation up to this point. The second input is the current element in the Stream.

On the first pass, the "result of the accumulation up to this point" is the identity. So it's 0 here.

After that, the "result of the accumulation up to this point" is the result of whatever it got from its previous iterations.

I've intuitvely named the variables currentScore (meaning the current Integer in the Stream) and accumulatedScore (meaning the result of the previous iterations). That makes it easier to figure out what they represent.

Now take a look at the right-hand side of the arrow in that lambda expression and you'll see how this works. The code simply adds the currentScore value to accumulatedScore.

That sum becomes the accumulatedScore in the next iteration. And so on.

Eventually, you end up with a sum of all the numbers in the Stream.

And that's your reduction.

Test, Don't Tease

Now run that code above against the dataset I referenced earlier and you should see this output:

13

And that's exactly what you'd expect based on the arithmetic you did in your head earlier.

Wrapping It Up

Now you know how to use reduce(). Just make sure you need to use it.

And that's your homework assignment. Look for a way to use reduce() that can't be handled with a convenience method in the Stream API.

And when you're done with that, have fun!

Photo by Burak K from Pexels