Domain / Business
Infrastructure
External Services
Events / Async
Security / Auth
Data / Storage
Domain Model + SIP State Machine
«aggregate root»MFOrder
- orderId: OrderId (UUID)
- clientId: ClientId
- canNumber: CANNumber
- scheme: SchemeCode
- orderType: MF_PURCHASE | MF_REDEEM | SIP | STP | SWP
- amount: Money (INR)
- units: BigDecimal?
- status: OrderStatus
- mfuRefNo: String?
- placedAt: Instant
- confirmedAt: Instant?
+ place(): DomainEvent[OrderPlaced]
+ confirm(mfuRef): DomainEvent[OrderConfirmed]
+ reject(reason): DomainEvent[OrderRejected]
+ cancel(): DomainEvent[OrderCancelled]
+ isWithinCutoff(): Boolean
«aggregate»SIPMandate
- mandateId: MandateId
- sipFrequency: MONTHLY | QUARTERLY
- sipDate: Int (1-28)
- amount: Money
- startDate / endDate: LocalDate
- status: SIPStatus
+ activate(): DomainEvent[SIPActivated]
+ pause(): DomainEvent[SIPPaused]
+ resume(): DomainEvent[SIPResumed]
+ cancel(): DomainEvent[SIPCancelled]
+ nextExecutionDate(): LocalDate
SIP Status State Machine
DRAFT
CREATED
ACTIVE
PAUSED
CANCELLED
ACTIVE -> PAUSED (reversible) | PAUSED -> ACTIVE via resume()
MFU Central Order Flow (ISO 20022)
Outbound: Domain -> ACL Translator -> ISO 20022 XML -> MFU API
Inbound: MFU Response -> ACL Translator -> Domain Event
MFOrder
place() invoked
by PlaceOrderUseCase
->
ACL Translator
MfuOrderMapper
domain -> ISO 20022
->
ISO 20022 XML
sese.023.001.11
SecuritiesOrder
->
MFU API
POST /api/v2/order
mTLS + API Key
MFU Response
HTTP 200 + XML
OrderConfirmation
->
ACL Translator
ISO 20022 -> domain
MfuResponseMapper
->
order.confirm()
DomainEvent emitted
OrderConfirmed
->
Kafka
byld.distribution.events.order-confirmed
byld.portfolio.events.portfolio-updated
«anti-corruption layer»MfuGatewayAdapter
implements: MfuGateway (domain port)
- httpClient: RestClient (mTLS configured)
- xmlMapper: Iso20022XmlMapper
- circuitBreaker: Resilience4j (name="mfu")
+ placeOrder(cmd): MfuOrderResult
+ registerSIP(cmd): MfuSipResult
+ cancelOrder(orderId): void
+ checkStatus(ref): OrderStatusResponse
Cut-off Rules
Equity MF: 15:00 IST
Debt MF: 15:00 IST
Liquid: 13:30 IST
ELSS: 15:00 IST
After cut-off -> next NAV date
Resilience Config
timeout: 10s
retry: 3x (exp backoff)
circuit: 50% fail -> OPEN
fallback: QUEUED state
SLA target: p99 < 3s
SIP Lifecycle + Cron Triggers
1. User Initiates SIP via MIA
MIA chatbot collects: scheme, amount, frequency,
start date, mandate type (eNACH/UPI autopay)
2. Create SIPMandate + Payment Mandate
CreateSIPUseCase -> SIPMandate.create()
Publishes: byld.distribution.events.sip-mandate-created -> byld-payments
3. Register with MFU Central
MfuGateway.registerSIP() -> MFU returns SIP Reg No
Status transitions: DRAFT -> CREATED
4. Monthly Cron Execution
SipExecutionScheduler runs daily at 06:00 IST
Checks: sipDate == today && status == ACTIVE
@Scheduled(cron = "0 0 6 * * *")
5. Trigger Payment Debit
Publishes: byld.distribution.events.sip-execution-due -> byld-payments
Payments debits via Razorpay mandate
Kafka: byld.distribution.events.sip-execution-due
6. Place MF Order on Payment Confirmation
Listens: byld.payments.events.payment-confirmed -> PlaceOrderUseCase
MFU order placed with SIP Reg No reference
7. NAV Allotment (T+1/T+2)
MFU callback / polling -> units allotted at NAV
Publishes: byld.distribution.events.order-confirmed -> byld-portfolio
8. Reconciliation Cron
Daily 21:00 IST: match MFU confirmations
with pending orders, flag discrepancies
@Scheduled(cron = "0 0 21 * * *")
Ditto Insurance + Saga Compensation
Ditto Insurance ACL
Quote API:
GET /api/v1/quotes (term/health)
Apply API:
POST /api/v1/applications
Scheduling:
Calendly webhook -> slot booked
Circuit:
Resilience4j (5s timeout, 2x retry)
Fallback:
Cache last quotes (24h TTL)
«port»InsuranceGateway
+ getQuotes(profile): List<Quote>
+ applyPolicy(cmd): ApplicationResult
+ scheduleCall(slot): CalendlyResult
+ getPolicyStatus(id): PolicyStatus
DigitInsuranceAdapter
implements InsuranceGateway
- restClient + circuitBreaker
- cacheManager (Redis, 24h TTL)
Saga: MF Order + Payment (Forward Flow)
1. Reserve
lock amount
->
2. Debit
Razorpay charge
->
3. MFU Order
place order
->
4. Confirm
portfolio update
Compensation (Reverse Flow)
4. FAIL
MFU rejects
<-
refund payment
<-
release reserve
<-
notify user
Kafka topics: byld.distribution.events.order-compensation-refund | byld.distribution.events.order-compensation-release | byld.notification.events.send
Package Structure (Clean Architecture)
com.byld.distribution
/domain
/model MFOrder, SIPMandate, CANNumber, SchemeCode
/event OrderPlaced, OrderConfirmed, SIPActivated
/port OrderRepository, MfuGateway, InsuranceGateway
/service CutoffTimeValidator, SipScheduleCalculator
/application
/usecase PlaceOrderUseCase, CreateSIPUseCase, CancelSIPUseCase
/saga OrderPaymentSaga, SipExecutionSaga
/infrastructure
/persistence JpaOrderRepository, JpaSipMandateRepository
/gateway MfuGatewayAdapter, DigitInsuranceAdapter
/acl Iso20022XmlMapper, MfuResponseMapper
/scheduler SipExecutionScheduler, ReconciliationScheduler
/api
/rest OrderController, SIPController
/kafka PaymentConfirmedListener, OrderEventPublisher
Database
byld_distribution (PostgreSQL)
Tables: mf_orders, sip_mandates,
can_registrations, order_events
Kafka Topics
byld.distribution.events.order-placed
byld.distribution.events.order-confirmed
byld.distribution.events.sip-mandate-created
byld.distribution.events.sip-executed
byld.distribution.events.sip-execution-due
Architect's Notes
"The center of your application is not the database. Nor is it one or more of the frameworks you may be using."
-- Robert C. Martin, Clean Architecture (2017)
Key Decisions:
1. ISO 20022 mapping in ACL, never in domain
2. SIP state machine enforced by aggregate
3. Saga choreography via Kafka (no orchestrator)
4. Cut-off time is a domain rule, not infra
5. Ditto Insurance behind port interface
6. Reconciliation as eventual consistency
7. Kotlin CMP for client-side presentation
Stability Metrics:
Afferent Coupling (Ca): 4 (portfolio, payments, mia, gateway)
Efferent Coupling (Ce): 2 (MFU, Digit)
Instability: Ce/(Ca+Ce) = 0.33 (stable)
Abstractness: 0.6 (6 ports / 10 classes)