Domain / Business
Infrastructure
External Services
Events / Async
Security / Auth
Data / Storage
Domain Model + Payment State Machine
«aggregate root»Subscription
- subscriptionId: SubscriptionId (UUID)
- clientId: ClientId
- tier: MASTER | ONE | EDGE | LEGACY
- billingCycle: MONTHLY | ANNUAL
- amount: Money (INR + GST)
- razorpaySubId: String?
- status: ACTIVE | PAST_DUE | CANCELLED
- currentPeriodStart: LocalDate
- currentPeriodEnd: LocalDate
- autoRenew: Boolean
+ activate(): DomainEvent[SubscriptionActivated]
+ upgrade(newTier): DomainEvent[TierUpgraded]
+ downgrade(newTier): DomainEvent[TierDowngraded]
+ cancel(): DomainEvent[SubscriptionCancelled]
+ renew(): DomainEvent[SubscriptionRenewed]
+ markPastDue(): DomainEvent[PaymentOverdue]
«aggregate»Payment
- paymentId: PaymentId
- subscriptionId: SubscriptionId?
- sipMandateId: MandateId?
- amount: Money
- razorpayOrderId: String
- razorpayPaymentId: String?
- status: PaymentStatus
- method: UPI | ENACH | CARD | NETBANKING
+ authorize(): DomainEvent[PaymentAuthorized]
+ capture(): DomainEvent[PaymentCaptured]
+ refund(amount): DomainEvent[PaymentRefunded]
+ fail(reason): DomainEvent[PaymentFailed]
Payment State Machine
CREATED
AUTHORIZED
CAPTURED
webhook: authorized
auto-capture 5min
CREATED
FAILED
CAPTURED
REFUNDED
timeout / bank decline
compensation saga
Razorpay Integration Flow
Full payment lifecycle: Order creation → Checkout → Webhook → Capture → Domain event
Create Order
InitiatePaymentUseCase
amount + currency + receipt
Razorpay API
POST /v1/orders
returns order_id
Checkout
Kotlin CMP SDK opens
Razorpay Checkout
User Pays
UPI / eNACH / Card
bank processes
Webhook
payment.authorized
HMAC-SHA256 verified
Verify Signature
RazorpayWebhookHandler
idempotency check
Auto-Capture
POST /v1/payments
/{id}/capture
Domain Event
PaymentCaptured
→ Kafka topic
«anti-corruption layer»RazorpayPaymentAdapter
implements: PaymentGateway (domain port)
- razorpayClient: RazorpayClient (API key + secret)
- webhookSecret: String (HMAC verification)
- circuitBreaker: Resilience4j ("razorpay")
+ createOrder(amount, currency, receipt): RzpOrderId
+ capturePayment(paymentId, amount): CaptureResult
+ refund(paymentId, amount): RefundResult
+ verifyWebhook(payload, signature): Boolean
+ createMandate(amount, method): MandateResult
Webhook Security
HMAC-SHA256 signature verification
IP whitelist: Razorpay CIDRs
Replay protection: event_id dedup
Retry: Razorpay retries 24h (exp backoff)
Idempotency
event_id stored in processed_events table
Unique constraint: razorpay_payment_id
At-least-once delivery → idempotent handler
TTL: 7 days for dedup records
Resilience
timeout: 8s (Razorpay SLA: 800ms)
retry: 2x (idempotency key)
circuit: 40% fail rate → OPEN
fallback: queue for async retry
Tier Upgrade Flow
Master
Free
One
3,650/yr
Edge
18,250/yr
Legacy
36,500/yr
Upgrade Sequence
Step 1
User Selects New Tier
MIA presents upgrade options with feature comparison
Pro-rated amount calculated for mid-cycle upgrade
Step 2
Initiate Payment
UpgradeTierUseCase creates Razorpay order
Amount = prorated difference + GST (18%)
Step 3
Payment Captured
Webhook: payment.captured → ProcessPaymentUseCase
subscription.upgrade(newTier) called
Step 4
Activate New Tier
DomainEvent: TierUpgraded published to Kafka
byld-identity updates RBAC permissions
Step 5
Unlock Features
byld-advisory: unlock advanced goal planning
byld-mia: unlock premium MIA persona
byld-portfolio: unlock detailed analytics
TierUpgraded Event (Kafka)
{ clientId, oldTier, newTier, effectiveDate,
  proratedAmount, subscriptionId }
Consumers: byld-identity, byld-advisory, byld-mia,
byld-portfolio, byld-notifications
GST Compliance
CGST: 9% | SGST: 9% | Total: 18%
HSN Code: 997159 (Financial advisory services)
Auto-generated tax invoice via byld-notifications
SIP Payment Cron Pipeline
Daily 06:00 IST: Check due mandates → Debit via Razorpay → Trigger MFU order → Update portfolio
Cron Trigger
06:00 IST daily
SipPaymentScheduler
Query Mandates
Find ACTIVE SIPs
where sipDate = today
Razorpay Debit
Auto-debit via
UPI/eNACH mandate
Kafka Event
byld.payments.events
.payment-confirmed
MFU Order
byld-distribution
places MF order
Failure Handling
Debit fails → Retry at 10:00, 14:00 IST (same day)
3 consecutive failures → SIP status = PAUSED
Publishes: byld.payments.events.sip-payment-failed → byld-notifications
MIA sends: "Your SIP for [scheme] failed. Please check your bank balance."
After 3 months paused → auto-cancel with user notification
Mandate Types
UPI AutoPay: max 15,000/txn, instant
eNACH: up to 1,00,000/txn, T+1 settlement
Razorpay Subscription: auto-debit on due date
Mandate registration requires one-time auth from user
Mandate revocation → SIP auto-pause
com.byld.payments / domain{Subscription, Payment, PaymentGateway} / application{InitiatePaymentUseCase, UpgradeTierUseCase, ProcessWebhookUseCase} / infrastructure{RazorpayPaymentAdapter, JpaPaymentRepository, SipPaymentScheduler, RazorpayWebhookHandler}
Architecture Notes
"A good architecture allows major decisions to be deferred."
-- Robert C. Martin, Clean Architecture (2017)
Key Decisions:
1. Razorpay behind PaymentGateway port (swappable to Juspay/Stripe)
2. Webhook handler is idempotent (event_id dedup)
3. Auto-capture after authorization (no manual step)
4. Tier logic is domain rule, not payment provider logic
5. SIP cron in payments, MFU order in distribution (separation)
6. GST calculation is a domain service, not infra
Stability Metrics:
Afferent Coupling (Ca): 6 (distribution, identity, advisory, mia, portfolio, notifications)
Efferent Coupling (Ce): 1 (Razorpay)
Instability: Ce/(Ca+Ce) = 0.14 (very stable)
Abstractness: 0.7 (PaymentGateway, MandateRepository, etc.)
Database
byld_payments (PostgreSQL)
Tables: subscriptions, payments,
mandates, processed_events, invoices
Kafka Topics
byld.payments.events.payment-confirmed
byld.payments.events.payment-failed
byld.payments.events.tier-upgraded
byld.payments.events.tier-downgraded
byld.payments.events.sip-payment-failed
byld.payments.events.subscription-renewed
byld.payments.events.subscription-cancelled