Custom Aggregations
An IAggregation defines how two partial results are combined into a single value. It is the building
block of every Aggregator measure — the leaves of the measure DAG that query the underlying table
directly.
@FunctionalInterface
public interface IAggregation {
Object aggregate(Object left, Object right);
}
The engine reduces an arbitrary number of rows by applying aggregate recursively:
aggregate(aggregate(row1, row2), row3), and so on. Implementations must therefore be associative.
Built-in aggregations
| Key | Class | Behaviour |
|---|---|---|
SUM |
SumAggregation |
Numeric sum; promotes int→long, float→double |
COUNT |
CountAggregation |
Counts non-null values |
AVG |
AvgAggregation |
Arithmetic mean |
max |
MaxAggregation |
Maximum (comparable) |
min |
MinAggregation |
Minimum (comparable) |
PRODUCT |
ProductAggregation |
Numeric product |
COALESCE |
CoalesceAggregation |
Returns the first non-null value |
SUM_ELSE_SET |
SumElseSetAggregation |
Sums numbers; collects non-numeric values in a Set instead of failing |
Use these by name as the aggregationKey of an Aggregator:
Aggregator.builder()
.name("revenue")
.aggregationKey(SumAggregation.KEY) // "SUM"
.build()
Custom aggregations: the MarketRiskSensitivity example
Standard numeric aggregations are not enough when the values being aggregated are domain objects with their own merging logic. A common example in financial risk is market risk sensitivities — structured objects that map multidimensional risk buckets (tenor × maturity) to delta values.
The domain object
@Value
@Builder
public class MarketRiskSensitivity {
/** Maps coordinate sets (e.g. {tenor=1Y, maturity=2Y}) to their delta. */
@Default
Object2DoubleMap<Map<String, ?>> coordinatesToDelta = Object2DoubleMaps.emptyMap();
public static MarketRiskSensitivity empty() { ... }
public MarketRiskSensitivity addDelta(Map<String, ?> coordinates, double delta) { ... }
/** Combines two sensitivities by summing deltas for matching coordinates. */
public MarketRiskSensitivity mergeWith(MarketRiskSensitivity other) { ... }
}
A single row might carry:
MarketRiskSensitivity.empty()
.addDelta(Map.of("tenor", "1Y", "maturity", "2Y"), 12.34)
The aggregation
public class MarketRiskSensitivityAggregation implements IAggregation {
@Override
public Object aggregate(Object left, Object right) {
if (left == null) return right;
if (right == null) return left;
return ((MarketRiskSensitivity) left).mergeWith((MarketRiskSensitivity) right);
}
}
The null-guards make aggregation safe when some rows are missing a sensitivity. The core logic
delegates to mergeWith(), an instance method on the domain object itself — a clean way to keep
aggregation logic co-located with the type that owns the merging semantics.
Registering the aggregator
Reference the implementation by its fully-qualified class name:
Aggregator.builder()
.name("sensitivities")
.columnName("sensitivities")
.aggregationKey(MarketRiskSensitivityAggregation.class.getName())
.build()
The full end-to-end example — including group-by tenor, filter by maturity, and explain output — is
demonstrated in TestPartitionor_PnLExplain.
How the engine resolves a custom aggregationKey
StandardOperatorFactory.makeAggregation() handles the lookup:
- Built-in keys (
"SUM","COUNT", …) are matched by aswitchstatement. -
Everything else is treated as a fully-qualified class name and instantiated via reflection:
-
If the class has a
Map<String, ?>constructor, it is called withaggregationOptions. - Otherwise the no-arg constructor is used.
This means any IAggregation implementation on the classpath can be referenced without registering
it anywhere — just pass its class name as the aggregationKey.
To share a custom key string (rather than repeating the class name), define a constant:
public class MarketRiskSensitivityAggregation implements IAggregation {
public static final String KEY = MarketRiskSensitivityAggregation.class.getName();
// or a shorter alias registered in a custom IOperatorsFactory
}
Aggregation logic placement: static vs instance method
Two natural patterns exist for where to put the merge logic:
| Pattern | Example | When to use |
|---|---|---|
| Instance method on the domain object | left.mergeWith(right) |
When the domain object owns its algebra and is under your control |
| Static method | Sensitivity.merge(left, right) |
When the domain object is a third-party type you cannot modify |
Both produce the same IAggregation implementation — the difference is only where the logic lives.
Further reading
TestPartitionor_PnLExplain— full scenario withMarketRiskSensitivityaggregated across colors and risk bucketsTestTableQuery_DuckDb_customAggregation— custom aggregation over DuckDB-backed data- Operators Factory — registering a custom key alias via
IOperatorsFactory - Partitionor — using a custom aggregation as the re-aggregation step after per-partition combination