Why Unified Portfolio Analytics is Hard
At first glance, consolidating portfolio performance across asset classes looks straightforward. A family office tracks traditional securities (stocks, bonds), private equity fund commitments (as a Limited Partner), and alternative assets (yachts, art, real estate). For reporting purposes, you need to answer basic questions: What is my total wealth? What's my return across all holdings? How much cash went in and out this year?
From a reporting perspective, this seems entirely achievable. Assets have values. Cash flows happen. Calculate the return. Done.
The real challenge begins when you examine how fundamentally different these asset classes are—not just financially, but in their semantic models, lifecycles, and performance measurement characteristics.
The First Layer: Valuation Semantics Differ Fundamentally
| Asset Class | Valuation Paradigm | Method | Confidence | Evidence |
|---|---|---|---|---|
| Traditional Securities | Continuous pricing | Market | High | Exchange price |
| Private Equity (LP Funds) | Quarterly NAV from GP | NAV | Medium | LP capital statement |
| Non-Bankable Assets (Real Estate, Artwork, Wine, Yachts) |
Discrete appraisal events (asset-type specific methodology) |
Appraisal | Low-Medium | Specialist appraisal (RE appraiser, art expert, etc.) |
Traditional securities live in a world of continuous pricing. A stock has a market price that updates every millisecond during trading hours. Valuation is not an event—it is a continuous state that can be sampled at any moment. The PriceCapturedEvent is merely a snapshot of market reality.
Private equity fund investments (LP positions) exist in a different temporal regime. As a Limited Partner, you do not have market prices for your fund positions. Instead, the General Partner (GP) provides quarterly Net Asset Value (NAV) statements showing your capital account balance. These are not observations of market consensus; they are calculated estimates combining mark-to-market holdings, discounted cash flow models for underlying portfolio companies, and GP-determined valuations. The LP must accept the GP's valuation methodology and timing.
Non-bankable assets occupy yet another valuation paradigm. A villa does not have a continuously observable price, nor does anyone send quarterly statements. Instead, valuation occurs through discrete appraisal events—perhaps annually for insurance purposes, when refinancing, or when contemplating sale. The appraisal methodology differs by asset type: real estate uses comparable sales and income approach, artwork requires specialist authentication and provenance verification, wine collections are valued by vintage and condition, yachts by age and maintenance history. The bounded context preserves these distinctions—an ArtworkAppraisalRecordedEvent carries different metadata (artist, provenance, condition report) than a RealEstateAppraisalRecordedEvent (square footage, location, comparable sales).
The Second Layer: Cash Flow Lifecycle Semantics Diverge
The Challenge
For bankable securities, cash flows are transactions: Buy, Sell, Income (dividends/coupons), and Expenses (fees). The lifecycle is straightforward.
Private equity LP investments operate under commitment accounting. As a Limited Partner, you commit capital to a fund (say, $10M) but do not transfer it immediately. Instead, the GP issues capital calls—irregular, unpredictable summons for portions of your commitment that must be funded within 10-15 days. Later, the GP sends distributions—returns of capital and profits, also irregular and unpredictable. Some distributions may be recallable (subject to clawback if the fund underperforms). The GP also deducts management fees (typically 2% of committed capital) and carried interest (typically 20% of profits) from your capital account.
Non-bankable assets introduce yet another lifecycle model, but crucially, each asset type has its own domain semantics. A villa is not the same as artwork, which is not the same as a wine collection:
- Real Estate (Villa): Property acquisition, ongoing property taxes, maintenance expenses, insurance, rental income from tenants, property management fees, renovation costs, eventual sale or transfer
- Artwork: Acquisition (purchase or gift), conservation expenses, insurance premiums, storage fees, authentication costs, provenance documentation, exhibition loans, sale through auction or private sale
- Wine Collection (Cellar): Bottle acquisition, cellar storage costs, temperature/humidity control expenses, insurance for collection, tasting events, bottle consumption (non-cash reduction), sale of bottles or cases
- Yachts: Purchase, crew salaries, mooring fees, maintenance and refits, insurance, charter income, fuel costs, eventual sale
Each asset type has specific terminology, lifecycle events, and expense categories that must be preserved in the domain model. Forcing "villa property tax" and "artwork conservation" into a generic "expense" category loses critical semantic information.
| Asset Class | Cash Flow Types | Semantic Weight |
|---|---|---|
| Bankable Securities | Buy, Sell, Income, Fees | Direct transactions |
| Private Equity (LP) | Capital Call, Distribution (Recallable/Final), Management Fee, Carried Interest | LP commitment accounting with irregular GP-driven cash flows |
| Non-Bankable Assets (Asset-type specific) |
Acquisition, Disposal, Expenses (asset-specific: property tax, conservation, cellar storage, crew salary), Income (rental, charter), Gift, Inheritance | Preserves domain semantics per asset type |
- A "distribution" (PE) is not the same as "selling shares" (securities)
- A "capital call" (PE) is not the same as "buying more" (securities)
- "Property tax" (villa) is not the same as "conservation" (artwork) or "cellar storage" (wine)
- Performance measurement and expense analysis must preserve these distinctions
The Third Layer: Ownership and Beneficiary Models Differ
Bankable Assets & Private Equity
Account-Based Model:
- Assets held in accounts
- Accounts belong to individuals
- Portfolios aggregate accounts
- Clear beneficiary chain: Portfolio → Account → Individual
Non-Bankable Assets
Direct Ownership Model:
- Assets owned directly (no accounts)
- Owner is legal titleholder
- Ownership transfers directly
- Cash flows associated with owner reference
This creates a beneficiary type mismatch. For unified portfolio reporting, you need to answer: "Show me all cash flows for John Doe across all asset types." But for securities and PE, John's cash flows are associated with his accounts. For non-bankable assets, they are associated with John as an owner. The data models are structurally incompatible.
The Fourth Layer: Time and Performance Measurement Differ
- Bankable securities support time-weighted returns (TWR) because valuations are frequent and reliable. TWR isolates manager skill from investor timing.
- Private equity LP investments use Internal Rate of Return (IRR) and LP-specific metrics like MOIC (Multiple on Invested Capital), DPI (Distributions to Paid-In Capital), and RVPI (Residual Value to Paid-In Capital). PE fund positions do not have continuous valuations, so TWR is impossible to calculate meaningfully. LPs also need liquidity planning through cash flow projections to anticipate future capital calls and distributions.
- Non-bankable assets also typically use IRR (or Total Return if held long enough), but with additional complexity around irregular expenses and infrequent appraisals.
The Portfolio-Level Problem
Now consider a portfolio containing all three asset classes. What is "the portfolio's return"?
- TWR is inappropriate because PE LP funds and NBA do not support frequent valuations
- IRR can be calculated across all asset types, but requires a common cash flow and valuation stream
- LP-specific metrics (MOIC, DPI, RVPI) only apply to PE fund positions
- Liquidity planning needs to incorporate both certain cash flows (securities) and projected cash flows (PE capital calls and distributions)
- This brings us back to the semantic mismatch problem
The Core Layer: Domain Purity vs. Unified Reporting
The problem is not that these differences exist. The problem is that they must coexist in two incompatible modes:
Domain Mode
Each bounded context (Deals, PE, NBA) models its own reality accurately. BuyCapturedEvent, PrivateEquityContributionCapturedEvent, and AssetAcquiredEvent are semantically distinct.
Forcing them to share a single event schema would create false equivalence and erode domain clarity.
Reporting Mode
Portfolio analytics requires a unified view. You cannot present three separate IRR calculations and ask the client to mentally aggregate them. You cannot show three separate cash flow reports with incompatible column schemas.
Unified reporting requires shared semantics.
Visualizing the Semantic Mismatch
Here's the challenge in a single view. Three bounded contexts, each with pure domain terminology, but reporting needs a unified view:
SellCapturedEvent
IncomeCapturedEvent
PriceCapturedEvent"] D2["• Account-based
• Continuous pricing
• TWR calculation
• Market method"] end subgraph "PRIVATE EQUITY LP Context - Pure LP Fund Domain" P1["LPCapitalCallCapturedEvent
LPDistributionCapturedEvent
LPRecallableDistributionCapturedEvent
LPManagementFeeCapturedEvent
LPCarriedInterestCapturedEvent
LPCapitalAccountNAVCapturedEvent"] P2["• LP commitment accounting
• Quarterly GP-provided NAV
• IRR, MOIC, DPI, RVPI
• Unfunded commitment tracking
• Liquidity projection"] end subgraph "NON-BANKABLE ASSETS Context - Pure NBA Domain" N1["AssetAcquiredEvent
AssetSoldEvent
PropertyTaxPaidEvent
ArtworkConservationEvent
CellarStorageFeeEvent
YachtCrewSalaryEvent
RealEstateAppraisalEvent
ArtworkAppraisalEvent"] N2["• Direct ownership
• Asset-type specific semantics
• Villa ≠ Artwork ≠ Wine ≠ Yacht
• Preserves domain terminology
• Specialist appraisal methods"] end REPORT["❌ UNIFIED REPORTING NEEDS:
• Common cash flow semantics
• Common valuation semantics
• Portfolio-level IRR
• Single beneficiary model"] D1 -.->|"Different
terminology"| REPORT P1 -.->|"Different
lifecycle"| REPORT N1 -.->|"Different
ownership"| REPORT style D1 fill:#e7f3ff,stroke:#0066cc,stroke-width:2px style D2 fill:#e7f3ff,stroke:#0066cc style P1 fill:#d4edda,stroke:#28a745,stroke-width:2px style P2 fill:#d4edda,stroke:#28a745 style N1 fill:#fff3e0,stroke:#ff9800,stroke-width:2px style N2 fill:#fff3e0,stroke:#ff9800 style REPORT fill:#f8d7da,stroke:#dc3545,stroke-width:3px
The Traditional Approach (And Why It Accumulates Debt)
The conventional solution—implementing reporting as cross-context queries with manual semantic translation—works but creates maintenance burden:
- Every new cash flow type requires updating multiple report generators
- Every new valuation method requires extending the unified query logic
- The reporting layer becomes a tangled web of if-statements and context-specific branches
- Adding a fourth asset class requires revisiting every reporting component
- The complexity grows combinatorially
The Solution: Event-Driven Shared Semantic Layer
The Key Insight
While domain events must remain semantically pure within their bounded contexts, they can project into a shared read model with normalized semantics. The key is that this normalization happens after the domain events are persisted, through subscribing projections—not by contaminating the domain model itself.
Shared Semantic Contracts
Two read models are defined in a shared kernel:
CashFlowRecord
Represents any cash flow, regardless of source context:
SourceContext(Deals / PE / NBA)FlowType(Buy, Sell, Contribution, Distribution, etc.)FlowSubType(ManagementFee, CarriedInterest, Storage, Crew)Amount(signed decimal)BeneficiaryRef/BeneficiaryType
ValuationRecord
Represents any valuation, regardless of source context:
SourceContext,AssetRefAmount,CurrencyValuationDateMethod(Market / Appraisal / NAV / Cost)Confidence(High / Medium / Low)
Important: These records are not domain aggregates. They are read models—derived views optimized for querying.
Event Projections Architecture: Preserving Domain Purity
The key insight: Domain events remain pure. Projections handle translation.
✓ SellCapturedEvent
✓ IncomeCapturedEvent
✓ PriceCapturedEvent"] end subgraph "PE CONTEXT - Pure LP Fund Domain Events" P_Events["✓ CapitalCallCapturedEvent
✓ DistributionCapturedEvent
✓ RecallableDistributionCapturedEvent
✓ ManagementFeeCapturedEvent
✓ CarriedInterestCapturedEvent
✓ LPCapitalAccountNAVCapturedEvent"] end subgraph "NBA CONTEXT - Pure Domain Events" N_Events["✓ AssetAcquiredEvent
✓ AssetSoldEvent
✓ ExpenseRecordedEvent
✓ ValuationRecordedEvent"] end subgraph "PROJECTION LAYER - Semantic Translation" D_Proj["Deals Projection
───────────
Buy → CashFlow(Buy)
Sell → CashFlow(Sell)
Price → Valuation(Market)"] P_Proj["PE LP Projection
───────────
CapitalCall → CashFlow(Contribution)
Distribution → CashFlow(Distribution)
ManagementFee → CashFlow(Expense)
LPCapitalAccountNAV → Valuation(NAV)"] N_Proj["NBA Projection
───────────
AssetAcquired → CashFlow(Acquisition)
PropertyTaxPaid → CashFlow(Expense, PropertyTax)
ArtworkConservation → CashFlow(Expense, Conservation)
CellarStorage → CashFlow(Expense, Storage)
RealEstateAppraisal → Valuation(Appraisal, RealEstate)
ArtworkAppraisal → Valuation(Appraisal, Artwork)"] end subgraph "SHARED READ MODELS - Normalized Semantics" CF["📊 CashFlowRecord
───────────
SourceContext: Deals/PE/NBA
FlowType: Buy/Contribution/Acquisition
Amount, Date, BeneficiaryRef"] VR["📊 ValuationRecord
───────────
Method: Market/NAV/Appraisal
Confidence: High/Medium/Low
Amount, Date, AssetRef"] end subgraph "UNIFIED ANALYTICS" Query["✅ Single Query Interface
───────────
GetCashFlows(portfolioId)
GetValuations(portfolioId)
CalculateIRR(portfolioId)"] end D_Events -->|Subscribe| D_Proj P_Events -->|Subscribe| P_Proj N_Events -->|Subscribe| N_Proj D_Proj -->|Emit| CF D_Proj -->|Emit| VR P_Proj -->|Emit| CF P_Proj -->|Emit| VR N_Proj -->|Emit| CF N_Proj -->|Emit| VR CF --> Query VR --> Query style D_Events fill:#e7f3ff,stroke:#0066cc,stroke-width:2px style P_Events fill:#d4edda,stroke:#28a745,stroke-width:2px style N_Events fill:#fff3e0,stroke:#ff9800,stroke-width:2px style D_Proj fill:#fff9e6,stroke:#ffcc00,stroke-width:2px style P_Proj fill:#fff9e6,stroke:#ffcc00,stroke-width:2px style N_Proj fill:#fff9e6,stroke:#ffcc00,stroke-width:2px style CF fill:#d1f2eb,stroke:#1abc9c,stroke-width:3px style VR fill:#d1f2eb,stroke:#1abc9c,stroke-width:3px style Query fill:#d6eaf8,stroke:#3498db,stroke-width:3px
- Top: Domain contexts use their own pure terminology (BuyCapturedEvent for securities, CapitalCallCapturedEvent for LP funds, PropertyTaxPaidEvent for villas, ArtworkConservationEvent for art, CellarStorageFeeEvent for wine)
- Middle: Projections translate but don't contaminate the domain—PropertyTaxPaidEvent projects to CashFlowRecord(Expense, PropertyTax) without forcing the domain to use generic terms
- Bottom: Unified read models with normalized semantics (CashFlowRecord with FlowSubType preserving the distinction, ValuationRecord with AssetType metadata)
- Result: Simple, context-agnostic queries for portfolio analytics including IRR, MOIC, DPI, RVPI, and expense analysis by asset type without losing domain precision
How Each Context Projects Events
Deals Context:
// BuyCapturedEvent → CashFlowRecord (FlowType=Buy, negative amount)
// SellCapturedEvent → CashFlowRecord (FlowType=Sell, positive amount)
// IncomeCapturedEvent → CashFlowRecord (FlowType=Income)
// PriceCapturedEvent → ValuationRecord (Method=Market, Confidence=High)
Private Equity LP Context:
// LP fund investments track commitment accounting from LP perspective
// LPCapitalCallCapturedEvent → CashFlowRecord
// (FlowType=Contribution, negative, reduces unfunded commitment)
// LPDistributionCapturedEvent → CashFlowRecord
// (FlowType=Distribution, positive, may be recallable)
// LPRecallableDistributionCapturedEvent → CashFlowRecord
// (FlowType=RecallableDistribution, marked as potentially reversible)
// LPManagementFeeCapturedEvent → CashFlowRecord
// (FlowType=Expense, FlowSubType=ManagementFee, typically 2% of commitment)
// LPCarriedInterestCapturedEvent → CashFlowRecord
// (FlowType=Expense, FlowSubType=CarriedInterest, typically 20% of profits)
// LPCapitalAccountNAVCapturedEvent → ValuationRecord
// (Method=NAV, Confidence=Medium, GP-provided quarterly statement)
// Used for calculating IRR, MOIC, DPI, RVPI and liquidity projections
Non-Bankable Assets Context (Preserving Asset-Specific Semantics):
// The NBA context preserves domain-specific terminology
// A villa is not the same as artwork, which is not the same as wine bottles
// Real Estate (Villa):
// VillaAcquiredEvent → CashFlowRecord (FlowType=Acquisition)
// PropertyTaxPaidEvent → CashFlowRecord (FlowType=Expense, FlowSubType=PropertyTax)
// PropertyMaintenanceEvent → CashFlowRecord (FlowType=Expense, FlowSubType=Maintenance)
// RentalIncomeReceivedEvent → CashFlowRecord (FlowType=Income, FlowSubType=Rent)
// RealEstateAppraisalRecordedEvent → ValuationRecord (Method=Appraisal, AssetType=RealEstate)
// Artwork:
// ArtworkAcquiredEvent → CashFlowRecord (FlowType=Acquisition)
// ArtworkConservationEvent → CashFlowRecord (FlowType=Expense, FlowSubType=Conservation)
// ArtworkStorageFeeEvent → CashFlowRecord (FlowType=Expense, FlowSubType=Storage)
// ArtworkAuthenticationEvent → CashFlowRecord (FlowType=Expense, FlowSubType=Authentication)
// ArtworkAppraisalRecordedEvent → ValuationRecord (Method=Appraisal, AssetType=Artwork)
// Wine Collection:
// WineBottlesAcquiredEvent → CashFlowRecord (FlowType=Acquisition)
// CellarStorageFeeEvent → CashFlowRecord (FlowType=Expense, FlowSubType=CellarStorage)
// BottlesConsumedEvent → No CashFlowRecord (consumption reduces quantity)
// WineCollectionAppraisalEvent → ValuationRecord (Method=Appraisal, AssetType=Wine)
// Yacht:
// YachtAcquiredEvent → CashFlowRecord (FlowType=Acquisition)
// YachtCrewSalaryEvent → CashFlowRecord (FlowType=Expense, FlowSubType=CrewSalary)
// YachtMaintenanceEvent → CashFlowRecord (FlowType=Expense, FlowSubType=Maintenance)
// CharterIncomeEvent → CashFlowRecord (FlowType=Income, FlowSubType=Charter)
// YachtAppraisalEvent → ValuationRecord (Method=Appraisal, AssetType=Yacht)
// Gift and Inheritance (all asset types):
// GiftRecordedEvent → No CashFlowRecord (ownership transfer without cash)
// InheritanceRecordedEvent → No CashFlowRecord (cost basis step-up without cash)
- PE's recallable distributions remain distinct in FlowType
- Management fees and carried interest are flagged in FlowSubType
- NBA's gift and inheritance transfers emit no cash flow (correctly reflecting their non-cash nature)
- Valuation Method preserves whether it's market, appraisal, or NAV
Unified Query Layer
Once projections populate the shared streams, a single query interface serves all reporting:
interface IPortfolioAnalyticsQuery {
Task<IReadOnlyList<CashFlowRecord>> GetCashFlowsAsync(
Guid? portfolioId,
Guid? beneficiaryRef,
SourceContext? sourceContext,
FlowType? flowType,
DateOnly? fromDate,
DateOnly? toDate, ...);
Task<IReadOnlyList<ValuationRecord>> GetValuationsAsync(
Guid? portfolioId,
Guid? assetRef,
SourceContext? sourceContext,
ValuationMethod? method,
DateOnly? fromDate,
DateOnly? toDate, ...);
Task<decimal> GetPortfolioValueAsync(
Guid portfolioId,
DateOnly asOfDate, ...);
}
All parameters are optional, enabling flexible queries:
- "All cash flows for Portfolio X across all contexts" →
GetCashFlowsAsync(portfolioId: X) - "Only PE contributions for Participation Y" →
GetCashFlowsAsync(assetRef: Y, sourceContext: PE, flowType: Contribution) - "NBA expense breakdown for Owner Z" →
GetCashFlowsAsync(beneficiaryRef: Z, beneficiaryType: Owner, flowType: Expense)
Performance Measurement Becomes Context-Agnostic
var cashFlows = await _query.GetCashFlowsAsync(portfolioId);
var latestValuation = await _query.GetPortfolioValueAsync(
portfolioId,
DateOnly.FromDateTime(DateTime.Today));
var irr = _irrCalculator.Calculate(cashFlows, latestValuation);
The IRR calculator does not care whether cash flows came from stock trades, capital calls, or yacht expenses. They are all CashFlowRecords with dates and amounts. The semantic translation happened upstream, in the projections.
Why This Works
1. Domain Events Remain Pure
PrivateEquityContributionCapturedEvent is still a PE-specific domain event, not contaminated with portfolio analytics concerns.
2. Projections Are Single-Responsibility
Each context's projection translates its own events. No cross-context coupling.
3. Reporting Decouples from Domain Changes
If PE adds a new fee type, only the PE projection updates. The query layer is unchanged.
4. Eventual Consistency is Acceptable
Projections run asynchronously. A small delay between event persistence and shared stream update is acceptable for reporting (unlike transactional consistency requirements in the command side).
5. Testability Through Reconciliation
Legacy context-specific queries can run in parallel with shared-stream queries during migration. Reconciliation tests validate parity within tolerance (e.g., ±$0.10 for amounts, ±1bp for IRR).
The Result: Unified Analytics Without Domain Contamination
Once the projection layer is in place, portfolio analytics becomes remarkably simple:
→ All cash flows across
Securities, PE, NBA"] Q2["GetValuations(portfolioId)
→ All valuations with
Method and Confidence"] Q3["CalculateIRR(portfolioId)
→ Portfolio-level return
across all asset classes"] end subgraph "SHARED READ MODELS" CF["CashFlowRecord Stream
─────────────
• BuyCapturedEvent → Buy
• LPCapitalCallCapturedEvent → Contribution
• AssetAcquiredEvent → Acquisition
• All normalized, all queryable"] VR["ValuationRecord Stream
─────────────
• PriceCapturedEvent → Market
• LPCapitalAccountNAVCapturedEvent → NAV
• ValuationRecordedEvent → Appraisal
• Method preserved, confidence explicit"] end subgraph "DOMAIN CONTEXTS REMAIN PURE" Pure["✓ Deals uses 'Buy' and 'Sell'
✓ PE uses 'CapitalCall' and 'Distribution' (LP perspective)
✓ NBA preserves asset-specific terminology:
- Villa: PropertyTax, RentalIncome
- Artwork: Conservation, Authentication
- Wine: CellarStorage, BottlesConsumed
- Yacht: CrewSalary, CharterIncome
✓ No cross-context terminology pollution"] end CF --> Q1 VR --> Q2 CF --> Q3 VR --> Q3 Q1 -.->|"Domain purity
maintained"| Pure Q2 -.->|"Unified semantics
achieved"| Pure Q3 -.->|"Analytics
simplified"| Pure style Q1 fill:#d6eaf8,stroke:#3498db,stroke-width:2px style Q2 fill:#d6eaf8,stroke:#3498db,stroke-width:2px style Q3 fill:#d6eaf8,stroke:#3498db,stroke-width:2px style CF fill:#d1f2eb,stroke:#1abc9c,stroke-width:2px style VR fill:#d1f2eb,stroke:#1abc9c,stroke-width:2px style Pure fill:#d5f4e6,stroke:#27ae60,stroke-width:3px
You achieve seemingly contradictory goals simultaneously:
- Domain Purity: Each bounded context uses its own terminology without compromise—PropertyTaxPaidEvent for villas, ArtworkConservationEvent for art, never generic "ExpensePaidEvent"
- Semantic Preservation: FlowSubType in CashFlowRecord preserves the distinction (PropertyTax, Conservation, CellarStorage) for expense analysis
- Unified Reporting: Portfolio analytics queries a single normalized stream while retaining the ability to drill down into asset-specific semantics
- Decoupling: Adding a fifth asset type (e.g., classic cars with their own terminology: Restoration, Vintage, Concours) only requires a new projection—existing query layer unchanged
- Testability: Projections can be tested independently with reconciliation validation
Conclusion
Unified portfolio analytics is not limited by the ability to query multiple data sources or aggregate results. It is limited by the requirement to:
- Preserve domain semantic purity while providing normalized reporting semantics
- Handle fundamentally different valuation epistemologies
- Respect distinct cash flow lifecycle models
- Unify beneficiary types without contaminating domain models
The traditional approach—implementing reporting as cross-context queries with manual branching—works but scales poorly. Each new asset class multiplies maintenance burden. Each new report type requires context-specific logic.
Domain contexts remain semantically pure. Projections translate into normalized schemas. Reporting queries a single unified stream. The architecture respects bounded context autonomy while enabling cross-context analytics.
In the end, you achieve the remarkable architectural feat of maintaining domain purity while providing unified reporting—without compromising either.
A triumph of event-driven design. A practical use of eventual consistency.