diff --git a/src/main/java/io/ventureplatform/util/InvestmentRollup.java b/src/main/java/io/ventureplatform/util/InvestmentRollup.java
new file mode 100644
index 0000000..eb71490
--- /dev/null
+++ b/src/main/java/io/ventureplatform/util/InvestmentRollup.java
@@ -0,0 +1,86 @@
+package io.ventureplatform.util;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Aggregates investment data across portfolio companies for dashboard rollups.
+ *
+ *
Used by reporting endpoints to compute category-level totals and rankings
+ * without round-tripping to the database for every aggregation.
+ */
+public final class InvestmentRollup {
+
+ /**
+ * Typed input for rollup calculations. Callers map their domain data
+ * (entities, DTOs, query rows) to this record at the boundary.
+ */
+ public record PortfolioInvestment(String category, long amount) { }
+
+ /**
+ * Result of {@link #buildCategoryFilter(List)}: a parameterised SQL fragment
+ * and the bound parameter values, ready to be passed to a
+ * {@code PreparedStatement} or {@code JdbcTemplate}.
+ */
+ public record CategoryFilter(String sql, List params) { }
+
+ private InvestmentRollup() {
+ // utility class
+ }
+
+ /**
+ * Sum the total investment amount for portfolio companies matching the
+ * given category. Returns 0 if no matches.
+ */
+ public static long totalInvestmentByCategory(
+ final List companies, final String category) {
+ long total = 0;
+ for (PortfolioInvestment company : companies) {
+ if (Objects.equals(company.category(), category)) {
+ total += company.amount();
+ }
+ }
+ return total;
+ }
+
+ /**
+ * Find the top N companies by investment amount in the given category,
+ * sorted descending by amount.
+ *
+ * @throws IllegalArgumentException if {@code topN} is not positive
+ */
+ public static List topByInvestment(
+ final List companies,
+ final String category,
+ final int topN) {
+ if (topN <= 0) {
+ throw new IllegalArgumentException("topN must be positive, was " + topN);
+ }
+ return companies.stream()
+ .filter(c -> Objects.equals(c.category(), category))
+ .sorted((a, b) -> Long.compare(b.amount(), a.amount()))
+ .limit(topN)
+ .toList();
+ }
+
+ /**
+ * Build a parameterised SQL filter for the given categories. The returned
+ * fragment uses {@code ?} placeholders so callers can bind values through a
+ * prepared statement and avoid SQL injection.
+ *
+ * If {@code categories} is null or empty, returns a clause that matches
+ * nothing ({@code 1=0}) with no parameters, so callers can compose it
+ * unconditionally without producing invalid SQL.
+ */
+ public static CategoryFilter buildCategoryFilter(final List categories) {
+ if (categories == null || categories.isEmpty()) {
+ return new CategoryFilter("1=0", List.of());
+ }
+ String placeholders = String.join(", ",
+ Collections.nCopies(categories.size(), "?"));
+ return new CategoryFilter(
+ "category IN (" + placeholders + ")",
+ List.copyOf(categories));
+ }
+}