Optimising multiple streams to single loop I am trying to find the best way to optimise the converters below to first follow the flow I call convertAndGroupForUpdate, which triggers the conversions and relevant mappings.

Any help to optimise this code would be massively appreciated.

public List&lt;GroupedOrderActionUpdateEntity&gt; convertAndGroupForUpdate(List&lt;SimpleRatifiableAction&gt; actions) {
    List&lt;GroupedOrderActionUpdateEntity&gt; groupedActions = new ArrayList&lt;&gt;();

    Map&lt;String, List&lt;SimpleRatifiableAction&gt;&gt; groupSimple = actions.stream()
        .collect(Collectors.groupingBy(x -&gt; x.getOrderNumber() + x.getActionType()));

    groupSimple.entrySet().stream()
        .map(x -&gt; convertToUpdateGroup(x.getValue()))
        .forEachOrdered(groupedActions::add);

    return groupedActions;
}

public GroupedOrderActionUpdateEntity convertToUpdateGroup(List&lt;SimpleRatifiableAction&gt; actions) {
    List&lt;OrderActionUpdateEntity&gt; actionList = actions.stream().map(x -&gt; convertToUpdateEntity(x)).collect(Collectors.toList());

    return new GroupedOrderActionUpdateEntity(
        actions.get(0).getOrderNumber(),
        OrderActionType.valueOf(actions.get(0).getActionType()),
        actions.get(0).getSource(),
        12345,
        actions.stream().map(SimpleRatifiableAction::getNote)
            .collect(Collectors.joining(", ", "Group Order Note: ", ".")),
        actionList);
}

public OrderActionUpdateEntity convertToUpdateEntity(SimpleRatifiableAction action) {
    return new OrderActionUpdateEntity(action.getId(), OrderActionState.valueOf(action.getState()));
}
Specifying a downstream Collector for Collectors.groupingBy

There is an alternative Collectors.groupingBy(Function, Collector) method that lets you specify further steps that you want to do with the intermediary List&lt;SimpleRatifiableAction&gt; values after the grouping.

Then, with a bit of renaming, some help from method references, plus some convenience methods like having a SimpleRatifiableAction.getKey():

public String getKey() {
    return getOrderNumber() + getActionType();
}


You can have a method that reads:

// dropping method visibility modifier for brevity
List&lt;GroupedOrderActionUpdateEntity&gt; process(List&lt;SimpleRatifiableAction&gt; actions) {
    return new ArrayList&lt;&gt;(actions.stream()
            .collect(Collectors.groupingBy(SimpleRatifiableAction::getKey,
                    Collectors.collectingAndThen(Collectors.toList(), 
                                                  this::createUpdateEntity)))
            .values());
}


Looping once, aggregating multiple values

Inside convertToUpdateGroup(List), now renamed as createUpdateEntity(List), you are streaming twice on the List argument. While this shouldn't be an issue for most cases, there is still an option to just loop once should it be one of the remaining places to optimize (hopefully with some runtime analysis/micro-benchmarking to prove it). 

This is achieved by creating the StringJoiner instance yourself (instead of relying on Collectors.joining()). To avoid similar List.get(0) calls, you can also get a reference to it once.

Putting it altogether:

// dropping method visibility modifier for brevity
GroupedOrderActionUpdateEntity createUpdateEntity(List&lt;SimpleRatifiableAction&gt; actions) {
    SimpleRatifiableAction first = actions.get(0);
    StringJoiner joiner = new StringJoiner(", ", "Group Order Note: ", ".");
    List&lt;OrderActionUpdateEntity&gt; updateEntities = new ArrayList&lt;&gt;();
    actions.forEach(v -&gt; {
        joiner.add(v.getNote());
        updateEntities.add(v.createUpdateEntity());
    });
    return new GroupedOrderActionUpdateEntity(
            first.getOrderNumber(),
            OrderActionType.valueOf(first.getActionType()),
            first.getSource(),
            12345,
            joiner.toString(),
            updateEntities);
}


SimpleRatifiableAction.createUpdateEntity() is also another convenience method that you can consider:

public OrderActionUpdateEntity createUpdateEntity() {
    return new OrderActionUpdateEntity(getId(), OrderActionState.valueOf(getState()));
}