Implementation design · 001-portfolio-analytics
Real-time portfolio analytics
How we will build it: one append-only ledger, analytics as pure folds, exact integer money, on Java 21 + Quarkus.
001-portfolio-analytics
Created
Read time
Build the platform as an event-sourced core: a single append-only TradeLedger, and each
analytic a stateful in-memory projection that folds TradeEvent and PriceTick.
All money is exact integer fixed-point (scale 1e-4), so the fast path and a from-zero replay are the same
integer function and reconcile bit-for-bit — the whole reliability case rests on that. The stack is Java 21 + Quarkus
3.37.3. The single biggest risk is that the spike's in-memory hot tail is not the durable, cold-tiering store the
unlimited-history NFR ultimately needs — deferred deliberately and logged.
1 · Principles check
This umbrella carries the platform-wide principle conformance for all five specs; each child design checks only the principles most load-bearing for its slice and points here. Verified against v0.1.0 (enterprise, 20 principles).
| Principle | Compliance | Notes |
|---|---|---|
| P-1 · Clarity over cleverness | One idea — fold a ledger. No framework magic on the hot path. | |
| P-2 · Small, reversible changes | Sliced into five specs; each analytic is an independent projection addable/removable without touching the ledger. | |
| P-3 · Explicit over implicit | Money scale, denominator basis, and relief policy are all explicit, never inferred. | |
| P-4 · One set of conventions, enforced | Spotless/format + a single money representation; jacoco in the build. | |
| P-5 · Record the why | Decisions D-001..D-004 below carry context + consequences. | |
| P-6 · Tests accompany behavior | TDD per user story; reconcile-to-replay is the anchoring test. | |
| P-7 · Done means verified | Each slice closes its SC via a passing test; the platform is verified by the end-to-end reconcile run. | |
| P-8 · Ship dark by default | Analytics REST behind portfolio.analytics.enabled, default false. | |
| P-9 · Decisions are grounded | §3 evidence table; the stack decision rests on a toolchain probe run this turn. | |
| P-10 · The enforcement floor holds | Build fails on format, test, or coverage regression; no waivers. | |
| P-11 · Contract-first interfaces | OpenAPI generated via smallrye-openapi at /q/openapi; each analytic an independently versioned contract (spec FR-004). | |
| P-12 · Library-first boundaries | Domain (model/ledger/position/weights/tax) is pure Java with no Quarkus imports; the framework touches only api/ and the wiring bean. | |
| P-13 · Secure by default | Read-only analytics, no secrets, strict CSP inherited by artifacts; no user-supplied code path. | |
| P-14 · Least privilege | Ingest is a single logical writer; consumers get read views, never the mutable projection. | |
| P-15 · Structured observability | Micrometer timers on every fold step; Prometheus at /q/metrics — the SLO instrument. | |
| P-16 · Service level objectives | Every NFR across the five specs carries a <spec-slo>; the timers measure exactly those. | |
| P-17 · Semantic versioning | Module 1.0.0-SNAPSHOT; the analytic contracts version independently (FR-004). | |
| P-18 · Supply-chain hygiene | Pinned Quarkus BOM; dependency-check is a wired-but-optional build step for the spike. | |
| P-19 · Supply-chain provenance | n/a | SBOM + build attestation deferred for the spike — not exercised; would land before any real release. |
| P-20 · Accessibility conformance | n/a | Backend analytics platform with no end-user UI; the spec-family artifacts themselves are accessible HTML. |
2 · Technical context
- Language(s)
- Java 21 (Temurin 21.0.9 LTS).
- Frameworks
- Quarkus 3.37.3 (quarkus-rest, rest-jackson, arc, micrometer-registry-prometheus, smallrye-openapi); build via Maven.
- Architecture
- Event-sourced modular monolith: a ledger plus one projection per analytic. Domain is framework-free (P-12); Quarkus touches only
api/and the wiring bean. - Storage
- In-memory append-only hot tail for the spike (single writer). Durable + cold-tiering store deferred to
TBD-storage-engine. - Testing
- JUnit 5 unit tests on the pure domain (no Quarkus boot, fast) plus one
@QuarkusTestREST smoke via REST-assured. The anchoring test is reconcile-to-replay. - Coverage
- Diff-aware via jacoco: patch target ≥ 90% on touched domain code, and a change never lowers coverage of the lines it touches. No single universal %.
- Perf targets
- The umbrella envelope: tick-to-analytic p99 < 1 ms in-process; the child SLOs ladder beneath it (append 5 µs → position 25 µs → tax relief 40 µs → full-book reweight 500 µs).
- Constraints
- No floating point on the money path (exact integer fixed-point, scale 1e-4). Analytics off by default (ship dark). Single logical writer.
3 · Grounding & evidence
The design-bearing facts and how each was confirmed.
| Claim the design rests on | Source | Status | Finding / note |
|---|---|---|---|
| Java 21 and a current Quarkus are available in this environment | asdf where java temurin-21.0.9; quarkus-bom/maven-metadata.xml |
Temurin 21.0.9 present; 3.37.3 is the latest stable Final (3.38 is a CR). | |
| Exact integer money makes reconcile-to-replay bit-exact | spec.html#NFR-002 (reconcile to the cent) |
An integer fold has no rounding divergence between fast path and replay — they are the same function. | |
| Pure folds let a new analytic be added without touching the ledger | spec.html#FR-001, #SC-002 |
A projection depends only on the event stream; the ledger has no analytic-specific coupling. | |
| An in-memory hot tail meets the spike's append latency | The append micro-benchmark (deferred) | Plausible for an ArrayList/array append; not yet measured — accepted as risk R-1, and the durable store is deferred anyway. |
4 · Approach
The ledger is the spine. Producers append immutable TradeEvents; a market-data feed supplies
PriceTicks. The AnalyticsPlatform bean fans each event into three projections — the position engine,
the weight book, and the tax-lot book — every one a pure fold whose state at sequence N is a function of the
prefix [0, N]. Because the fold is pure and the arithmetic is exact integers, the same code that maintains the
live view also replays history to reconcile it. REST resources expose read views behind a feature toggle; Micrometer times
each fold so the latency SLOs are measured, not asserted.
5 · Alternatives considered
| Option | Fast boot + built-in metrics/OpenAPI | Exact-math fit (JVM) | Dev speed here | Total |
|---|---|---|---|---|
| Java 21 + Quarkus (chosen) | 5 | 5 | 5 | 15 |
| Java 21 + Spring Boot | 3 | 5 | 4 | 12 |
| Go or Rust service | 4 | 4 | 2 | 10 |
Quarkus wins on batteries-included observability (Micrometer) and contract-first OpenAPI with a fast dev loop, while the
JVM's long arithmetic gives us exact fixed-point money for free. Spring Boot is close but heavier to boot;
Go/Rust would cost dev velocity for no correctness gain here.
6 · Decisions
D-001 · Event-sourced projections over a single append-only ledger
- Status
Accepted - Context
- The spec's whole reliability case (
spec.html#FR-001,#NFR-002) is that a live analytic must always equal a from-zero replay. - Decision
- Model each analytic as a stateful in-memory projection that folds the ledger; the fast path and replay call the same fold function.
- Consequences
- + Reconcile-to-replay is a mechanical test, not a hope. + New analytics need no ledger change. − Projection state must be rebuildable/snapshot-able; no analytic may hold hidden non-fold state.
D-002 · Exact integer fixed-point money (scale 1e-4)
- Status
Accepted - Context
- "Reconcile to the cent" (
#NFR-002) is impossible to guarantee with binary floating point. - Decision
- Represent every price and monetary amount as a
longin units of 1e-4 currency (PRICE_SCALE = 10_000); quantities are wholelongs. Nodoubleon the money path; portfolio weights usedoubleonly for the final display ratio. - Consequences
- + Bit-exact folds and audit-grade realized gain. + Tax-lot basis needs no division (a lot's basis-per-unit is the exact fill price). − A hard invariant to police (no stray
double); − average-cost basis reduction uses deterministic integer division, which the position engine and tax-lot book reconcile in aggregate but decompose differently.
D-003 · Java 21 + Quarkus 3.37.3 on Maven
- Status
Accepted - Context
- Need built-in observability and contract-first OpenAPI with a fast loop; toolchain probe (§3) confirmed Java 21 + Quarkus 3.37.3 available.
- Decision
- Quarkus with rest, rest-jackson, arc, micrometer-registry-prometheus, smallrye-openapi; JUnit 5 + REST-assured; jacoco for coverage.
- Consequences
- + Metrics, OpenAPI, and DI out of the box; + JVM exact-integer math. − Quarkus is pinned to a Java-21-compatible release (avoid the bleeding-edge Java 25 default on this machine).
D-004 · In-memory hot tail for the spike; durable store deferred
- Status
Provisional - Context
- Unlimited history + durability (
#NFR-003, ledger#NFR-004) ultimately need a tiered durable store; the spike proves the folds, not the storage engine. - Decision
- Back the ledger with an in-memory append-only structure; model snapshot/restore so the durable engine can slot in later. Durability + cold-tiering deferred to
TBD-storage-engine. - Consequences
- + Fastest path to a working, testable platform. − The unlimited-history and no-loss SLOs are demonstrated in shape (snapshot bounds memory), not proven at scale — carried as risk R-1.
7 · Project structure
New paths, grouped by the D-001 boundary (framework-free domain vs. the thin Quarkus edge).
pom.xml Quarkus BOM + extensions + jacoco
src/main/resources/application.properties ship-dark toggle, metrics, openapi
src/main/java/io/spectastic/portfolio/
model/ Side, TradeEvent, PriceTick, Money (scale) # shared, spec 001
ledger/ TradeLedger, Snapshot # spec 002
position/ PositionEngine, Position # spec 003
weights/ PortfolioWeights, WeightBasis # spec 004
tax/ TaxLotBook, Lot, ReliefPolicy, RealizedGainRecord # spec 005
AnalyticsPlatform.java @ApplicationScoped wiring: ledger + projections
api/ AnalyticsResource + DTOs JAX-RS read surface (dark)
src/test/java/io/spectastic/portfolio/
LedgerTest, PositionEngineTest, PortfolioWeightsTest,
TaxLotBookTest, ReconcileToReplayTest, AnalyticsResourceIT
8 · Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| R-1 · In-memory ledger is not the durable, cold-tiering store unlimited history needs | H | M | Snapshot/restore modelled now so the durable engine slots in behind the same interface; scope-limited to the spike; deferred to TBD-storage-engine. |
R-2 · A stray double creeps into the money path and breaks reconcile-to-the-cent | M | H | Single money representation (D-002); reconcile-to-replay test would catch any drift; weights are the only sanctioned double, and only for a display ratio. |
| R-3 · Aggregate (position) and per-lot (tax) basis diverge | M | M | An explicit invariant test: Σ open-lot qty equals net position qty per instrument; the two are maintained separately by design. |
9 · Complexity tracking
Where this design adds anything beyond the minimum the specs demand.
Maintaining two basis decompositions — aggregate average-cost in the position engine and exact per-lot in the tax-lot book — is deliberate duplication, not accidental: they answer different questions (current exposure vs. which shares were sold) and the specs assign them to different slices. They are kept honest by the aggregate-agreement invariant (R-3), not merged.
10 · Open questions
None outstanding — the architecture (event-sourced folds), the money representation (exact integer fixed-point), and the
stack (Java 21 + Quarkus) were all settled and grounded this turn; storage durability is explicitly deferred to
TBD-storage-engine, not left open.
11 · Change log
- Initial design. Event-sourced projections over a single append-only ledger (D-001), exact integer fixed-point money at scale 1e-4 (D-002), Java 21 + Quarkus 3.37.3 (D-003), and an in-memory hot tail with durable storage deferred (D-004). Carries the platform-wide 20-principle check for the child designs.