Domain Model
Portfolio Aggregate
«Aggregate Root» Portfolio
- id : PortfolioId PK
- userId : UserId
- holdings : List<Holding>
- totalValue : Money
- investedAmount : Money
- xirr : Percentage
- lastSyncAt : Instant
+ addHolding(holding) : void
+ recordTransaction(txn) : void
+ computeXIRR() : Percentage
+ syncFromAA(data) : void
«Entity» Holding
- id : HoldingId
- isin : ISIN
- fundName : String
- units : BigDecimal
- avgNav : Money
- currentNav : Money
- currentValue : Money
- category : AssetCategory
- source : DataSource
+ unrealizedGain() : Money
+ gainPercent() : Percentage
«Entity» Transaction
- id : TransactionId
- holdingId : HoldingId
- type : TxnType {BUY, SELL, SIP, SWITCH, DIV}
- amount : Money
- units : BigDecimal
- nav : Money
- executedAt : Instant
- status : TxnStatus
«Value Object» Money
- paise : long
- currency : Currency = INR
Value Object Deep Dive
Money — "long paise" Representation
Money
All financial calculations use "long paise" internally. No floating point. No rounding errors. Ever.
Internal: 1_24_56_78_90 paise
Type: long (64-bit signed)
Max: 92,23,37,20,36,854.76
Currency: INR (ISO 4217)
Display Format
₹12,45,678.90
Indian numbering system (lakh, crore)
add(other: Money)
50,000.00 + 25,000.50
= 75,000.50
subtract(other: Money)
1,00,000.00 - 12,345.67
= 87,654.33
multiply(factor: BigDecimal)
1,00,000.00 * 1.12
= 1,12,000.00
allocate(ratios: int[])
1,00,000.00 / [60, 30, 10]
= [60K, 30K, 10K] (no loss)
isGreaterThan(other)
50,000.01 > 50,000.00
= true
display(locale: IN)
124567890 paise
= "₹12,45,678.90"
Morningstar NAV Sync
Scheduler
Morningstar
Transform
DB
Daily 23:00 IST | ~5000 schemes | bulk upsert
Account Aggregator Import
User
Finvu
Transform
Holdings
Consent → FI Fetch → ReBIT v2 → normalize → merge
CQRS Architecture
Write Model → Events → Read Model
Write Model
PostgreSQL
portfolio schema
Normalized 3NF
Strong consistency
Kafka
Event Store
Kafka + MSK
PortfolioUpdated
HoldingChanged
NAVRefreshed
Consumer
Read Model
Redis Cache
Dashboard JSON
Pre-computed
TTL: 60s
Performance Analytics Engine
+18.7%
XIRR
Annualized return
+15.2%
CAGR
3-year compound
1.12
Sharpe
Risk-adjusted
+3.2%
Alpha
vs Nifty 50 TRI
0.87
Beta
Market sensitivity
-8.4%
Max DD
Max drawdown
Benchmark Comparison
Your Portfolio +18.7%
Nifty 50 TRI +15.5%
CRISIL Hybrid +12.1%
FD (SBI 1Y) +7.1%
REST API Endpoints
EndpointDescriptionNotes
GET/api/v1/portfolioDashboard summaryRedis read model
GET/api/v1/portfolio/holdingsAll holdings with current valuePaginated
GET/api/v1/portfolio/holdings/{id}/transactionsTransaction history for holdingCursor-based
GET/api/v1/portfolio/performanceXIRR, CAGR, benchmark comparisonComputed daily
POST/api/v1/portfolio/import/aaTrigger AA import via FinvuAsync, consent req
POST/api/v1/portfolio/syncForce Morningstar NAV refreshRate limited
Domain Events (Kafka)
PortfolioCreated HoldingAdded HoldingRemoved TransactionRecorded NAVRefreshed AAImportCompleted XIRRComputed DashboardCacheInvalidated
Topic: byld.portfolio.events
Partitions: 6 (by userId hash)
Retention: 7 days
Consumer groups: advisory, mia, dashboard
Architect's Notes
Money as "long paise" is non-negotiable. BigDecimal is tempting but slow. IEEE 754 double is unacceptable for financial math. The allocate() method handles Foemmel's Conundrum (splitting ₹1.00 three ways without losing a paisa).

CQRS separates the write path (transactions, syncs) from the read path (dashboard, charts). Redis read model is rebuilt from events -- eventually consistent but renders the dashboard in <50ms.
"If you're doing money arithmetic with doubles,
you will lose money." -- Joshua Bloch, Effective Java
Legend
Domain / Business
Infrastructure
Events / Async
External Services
Security / Auth
Data / Storage