romanzipp/laravel-projectable-aggregates
Laravel package for projecting aggregate values such as counts, sums, and averages from related Eloquent models onto a parent model. Define projections declaratively and keep denormalized columns in sync automatically, avoiding repeated aggregate queries and simplifying reporting.
Architecture fit. The package addresses a real and recurring gap in Eloquent: persisted denormalized aggregate columns (counts, sums, averages, min/max) on a parent model, rather than recomputing them via withCount/withSum subqueries on every request. Architecturally it is a conventional Laravel extension — service provider, publishable config/migrations, a Projectable trait on the parent, and a Projector base class where each aggregate (Count, Sum, Avg, Min, Max) is declared. That is the standard, well-understood Laravel shape and fits cleanly into a monolith or modular monolith. It does not fit systems where projection state is already owned elsewhere (event sourcing, CQRS with a dedicated read model) or where the write path bypasses Eloquent. As a TPM, the framing is: this replaces bespoke observer/denormalization code with a conventional implementation — a build-vs-buy call on a pattern the team already knows, not an architectural bet.
Integration feasibility. Mechanically trivial: composer require, publish, migrate, add the trait plus aggregate columns, define projectors, run the recompute command. The install is an afternoon. The decision and the sequencing around it are the real work — choosing which relations become projectable, executing backfill safely, and instrumenting drift. There is essentially no ecosystem signal to lean on: 13 stars, 0 dependents, single maintainer. Treat the source (small surface) as the documentation and audit it before trusting it.
Technical risk.
insert/upsert, raw SQL, external services, DB-level cascades, data-fix scripts) silently desynchronizes the stored value. This is a product-visible correctness risk, not just a technical one.increment()/decrement(). Must be verified in source.Key questions before adoption.
created/updated/deleted/restored/forceDeleted) and are soft deletes handled?Stack fit. Strong fit for Laravel 9–11 on MySQL/Postgres where read-heavy list/dashboard/admin endpoints issue expensive aggregates over large child tables — "12 open tickets," "4.8 average rating," top-customers-by-order-count, inventory totals. Poor fit where data volume is small (just index and query), where exact real-time accuracy is contractual, or where write throughput is already the bottleneck.
Migration path.
Post.comments_count) behind a flag, keeping query-based reads live.chunkById job; throttle to avoid lock escalation.Compatibility. Check composer.json constraints and CI matrix against the target app; pin the framework or vendor the trait where the package lags. Because it is MIT and trait-shaped, forking into app/Support is a cheap and often preferable hedge.
Sequencing. Trait + columns (no reads) → shadow comparison → backfill → read cutover → reconciliation → expand to further models. Prerequisites: a working queue worker and scheduled-job capability, both required for safe backfill and queued recompute. Do not sequence the package ahead of those.
Maintenance. The dominant cost. A 13-star, zero-dependent, single-maintainer package should be treated as vendored code you happen to install via Composer. Budget for periodic Laravel major-version checks, fork-on-break, and internal ownership of the reconcile job. Keep it off financial, billing, or inventory-critical paths. The pitch to stakeholders is "small MIT utility we will own," not "community-supported dependency."
Support. No community to lean on; triage depends on maintainer availability. The effective support model is internal: own the semantics, read the (small) source, pin in composer.lock, and maintain a short ADR plus a "never mutate projected tables outside Eloquent" rule.
Scaling. Write amplification is the ceiling — every child mutation does work on the parent row, and hot parents (a team/user receiving thousands of child writes per minute) serialize on that row's lock. Mitigations: queue the recompute, batch increments, or shard the counter. Read-side scaling is excellent, which is the whole point: the trade is explicit, moving cost from read to write. Model it against the actual write profile before committing.
Failure modes to instrument.
Ramp-up. Low for a competent Laravel engineer — a trait plus a few config keys, learnable in an afternoon. The real ramp is conceptual: the team must internalize that these columns are caches with a truth source elsewhere, which changes how they debug and how they write bulk SQL. Encode that expectation before the first model ships.
Recommendation. Adopt selectively — read-heavy, high-fan-out aggregates on non-critical entities, with shadow validation, a nightly reconciliation job, and standing willingness to fork. Reject for billing, inventory, or anywhere a stale counter is a correctness incident. Given the adoption signals (13 stars, 0 dependents, low opportunity score), position it as a tactical lever to defer a larger reporting-store or read-replica initiative — not a strategic platform choice.
How can I help you explore Laravel packages today?