Domain Layer — Aggregates, Entities & Value Objects
«aggregate root»
Portfolio
id: PortfolioId
clientId: ClientId
holdings: List<Holding>
transactions: List<Transaction>
summary: PortfolioSummary
tier: Tier
createdAt: Instant
addHolding(h: Holding): void
recordTransaction(t: Transaction): void
recalculateSummary(): PortfolioSummary
getXIRR(): XIRR
getAssetAllocation(): Map
«entity»
Holding
id: HoldingId
isin: ISIN
assetClass: AssetClass
units: BigDecimal
avgCost: Money
currentNav: NAV
currentValue: Money
gainOrLoss(): Money
returnPct(): BigDecimal
updateNav(nav: NAV): void
«entity»
Transaction
id: TransactionId
holdingId: HoldingId
type: BUY | SELL | SIP | SWP
amount: Money
units: BigDecimal
nav: NAV
executedAt: Instant
orderId: OrderId?
isCredit(): boolean
stampCost(): Money
«value object»
Money
paise: long
currency: Currency (INR)
add(other: Money): Money
subtract(other: Money): Money
multiply(factor): Money
isPositive(): boolean
toString(): "₹12,45,678.90"
«value object»
ISIN
code: String
validate(): boolean
countryCode(): String
«value object»
AssetClass
type: EQUITY | DEBT |
GOLD | HYBRID | REIT
subType: String
«value object»
NAV
value: BigDecimal
asOf: LocalDate
isStale(): boolean
«value object»
XIRR
rate: BigDecimal
cashflows: List<CF>
annualized(): String
format(): "14.2%"
«value object»
PortfolioSummary
totalInvested: Money
currentValue: Money
totalGain: Money
xirr: XIRR
allocation: Map
lastUpdated: Instant
«domain events»
Portfolio Events
HoldingAdded
TransactionRecorded
PortfolioRebalanced
NAVUpdated
AADataImported
Application Layer — Use Cases & Ports
«use case»
GetPortfolioSummary
portfolioRepo: PortfolioRepository
fundDataPort: FundDataPort
execute(query): PortfolioSummaryDTO
«use case»
AddHolding
portfolioRepo: PortfolioRepository
eventPublisher: EventPublisherPort
execute(cmd: AddHoldingCmd): void
«use case»
RecordTransaction
portfolioRepo: PortfolioRepository
eventPublisher: EventPublisherPort
execute(cmd: RecordTxnCmd): void
«use case»
ImportFromAA
aaDataPort: AADataPort
portfolioRepo: PortfolioRepository
execute(cmd: ImportAACmd): ImportResult
«use case»
SyncWithMorningstar
fundDataPort: FundDataPort
portfolioRepo: PortfolioRepository
execute(): SyncResult
«port»
FundDataPort
getNAV(isin): NAV
getFundDetails(isin): FundData
getHistoricalNAV(): List<NAV>
«port»
AADataPort
fetchConsent(cId): ConsentArtifact
fetchFIData(consent): FIData
revokeConsent(cId): void
«port»
EventPublisherPort
publish(event: DomainEvent): void
publishBatch(events): void
«port»
PortfolioRepository
findById(id): Portfolio?
findByClient(cId): List
save(p: Portfolio): void
Infrastructure Layer — Adapters & External Systems
«adapter»
MorningstarAdapter
apiKey: String
webClient: WebClient
acl: MorningstarACL
getNAV(isin): NAV
getFundDetails(isin): FundData
«adapter»
FinvuAAAdapter
baseUrl: String
entityId: String
acl: FinvuACL
fetchConsent(cId): ConsentArtifact
fetchFIData(consent): FIData
«adapter»
JpaPortfolioRepository
entityManager: EntityManager
mapper: PortfolioMapper
findById(id): Portfolio?
findByClient(cId): List
save(p: Portfolio): void
«adapter»
KafkaEventPublisher
kafkaTemplate: KafkaTemplate
topicMapper: TopicMapper
publish(event): void
publishBatch(events): void
«adapter»
RedisPortfolioCache
redisTemplate: RedisTemplate
ttl: Duration = 5min
getCachedSummary(id): PortfolioSummary?
cacheSummary(id, s): void
API Layer — REST Controllers
«rest controller»
PortfolioController
GET /api/v1/portfolio/{id}
GET /api/v1/portfolio/{id}/summary
POST /api/v1/portfolio/{id}/holdings
POST /api/v1/portfolio/{id}/transactions
POST /api/v1/portfolio/import-aa
«scheduled»
NAVSyncScheduler
@Scheduled(cron = "0 0 20 * * MON-FRI")
syncAllNAVs(): void
Money Value Object — Deep Dive
public record Money(long paise, Currency currency) {

  // Factory method — never use doubles for money
  public static Money of(long rupees, int paise) {
    return new Money(rupees * 100 + paise, INR);
  }

  public Money add(Money other) {
    require(currency == other.currency);
    return new Money(paise + other.paise, currency);
  }

  public String toString() {
    // Indian number system: ₹12,45,678.90
    return "₹" + formatIndian(paise / 100)
      + "." + pad2(paise % 100);
  }

  // Immutable: all methods return new Money
  // Paise-based: no floating-point rounding errors
}
CQRS applied here:
Write model (Portfolio aggregate) persists to Aurora PostgreSQL.
Read model (PortfolioSummary) projected to Redis for sub-100ms dashboard loads.
Dependency
Implements
Composition
Domain Application Infrastructure API