Domain Model
Class Diagram
«Aggregate Root» User
- id : UserId PK
- phone : PhoneNumber
- email : Email
- tier : Tier {MASTER, EDGE, CORE, LEGACY}
- kycLevel : KYCLevel {NONE, MIN, FULL}
- devices : List<Device>
- status : UserStatus {ACTIVE, SUSPENDED, CLOSED}
- createdAt : Instant
+ bindDevice(device) : void
+ upgradeTier(tier) : void
+ completeKYC(record) : void
+ suspend(reason) : void
«Aggregate» KYCRecord
- id : KYCId PK
- userId : UserId
- pan : PAN
- aadhaar : AadhaarNumber
- status : KYCStatus {PENDING, VERIFIED, REJECTED}
- verifiedAt : Instant
- expiry : LocalDate
+ verify(provider, result) : void
+ reject(reason) : void
+ isExpired() : boolean
«Value Object» PhoneNumber
- countryCode : String
- number : String
+91 98XXX XXXXX
«Value Object» PAN
- value : String
ABCDE1234F
«Value Object» AadhaarNumber
- encryptedValue : byte[]
- maskedDisplay : String
XXXX XXXX 4321
+ masked() : String → "XXXX XXXX 4321"
+ decrypt(key) : String
«Port» KYCProviderPort
+ verifyPAN(pan) : KYCResult
+ verifyAadhaar(aadhaar) : KYCResult
+ checkKRAStatus(pan) : KRAStatus
«Port» SessionPort
+ createSession(user, device) : Session
+ invalidate(sessionId) : void
+ refresh(token) : TokenPair
OTP Login Flow
Sequence Diagram
User
Controller
LoginUseCase
OTPService
SMS Gateway
SessionPort
1. requestOTP(phone)
2. initOTPLogin(cmd)
3. generateOTP(phone)
4. sendSMS(otp)
delivered
otpRef
202 {otpRef, ttl:120}
User enters OTP on device
5. verifyOTP(ref, code)
6. verifyOTPLogin(cmd)
7. validate(ref, code)
VALID
8. createSession(user, device)
TokenPair {access_jwt, refresh_jwt}
LoginResult
200 {tokens, user}
PostgreSQL Schema
Database Tables
identity.users
idUUIDPK
phone_hashVARCHAR(64)UNIQUE
phone_encryptedBYTEANN
email_encryptedBYTEA
tierVARCHAR(10)NN
kyc_levelVARCHAR(10)NN
statusVARCHAR(10)NN
created_atTIMESTAMPTZNN
updated_atTIMESTAMPTZNN
identity.kyc_records
idUUIDPK
user_idUUIDFK IDX
pan_hashVARCHAR(64)UNIQUE
pan_encryptedBYTEA
aadhaar_hashVARCHAR(64)UNIQUE
aadhaar_encryptedBYTEA
statusVARCHAR(10)NN
providerVARCHAR(20)
verified_atTIMESTAMPTZ
expiry_dateDATE
identity.devices
idUUIDPK
user_idUUIDFK IDX
fingerprintVARCHAR(128)UNIQUE
platformVARCHAR(10)
push_tokenVARCHAR(256)
is_primaryBOOLEAN
bound_atTIMESTAMPTZNN
identity.sessions Redis-backed
session_idUUIDPK
user_idUUIDFK
device_idUUIDFK
refresh_hashVARCHAR(64)
expires_atTIMESTAMPTZ
ip_addressINET
idx_users_phone_hash (phone_hash) UNIQUE
idx_kyc_user_id (user_id)
idx_kyc_pan_hash (pan_hash) UNIQUE
idx_devices_user (user_id, is_primary)
idx_sessions_user (user_id, expires_at)
REST API Endpoints
MethodEndpointDescriptionAuth
POST /api/v1/auth/otp/request Request OTP for phone None
POST /api/v1/auth/otp/verify Verify OTP, issue tokens None
POST /api/v1/auth/refresh Refresh access token Refresh JWT
POST /api/v1/auth/mpin/setup Set MPIN for device Bearer JWT
POST /api/v1/kyc/verify Submit KYC documents Bearer JWT
GET /api/v1/kyc/status Check KYC verification status Bearer JWT
GET /api/v1/users/me Current user profile Bearer JWT
PUT /api/v1/users/me/devices Bind / update device Bearer JWT
DEL /api/v1/auth/logout Invalidate session Bearer JWT
Domain Events (Kafka: byld.identity.events)
byld.identity.events.user-registered byld.identity.events.otp-requested byld.identity.events.login-succeeded byld.identity.events.login-failed byld.identity.events.kyc-submitted byld.identity.events.kyc-verified byld.identity.events.kyc-rejected byld.identity.events.tier-upgraded byld.identity.events.device-bound byld.identity.events.session-created byld.identity.events.session-revoked byld.identity.events.user-suspended
Strategy Pattern: AuthStrategy
AuthStrategy OTPStrategy
BiometricStrategy
MPINStrategy
Runtime selection based on device capabilities
and user preference. OTP = fallback always.
Architect's Notes
Clean Architecture Boundaries
Domain layer has ZERO framework imports. KYCProviderPort and SessionPort are interfaces defined in the domain, implemented in infrastructure.

PII fields (phone, email, PAN, Aadhaar) are encrypted at rest (AES-256-GCM) with searchable HMAC-SHA256 hashes for lookup. Never log plaintext PII.
Security Invariants
• OTP: 6-digit, 120s TTL, max 3 attempts
• JWT: RS256, access 15m, refresh 30d
• Device binding: max 3 active devices
• Rate limit: 5 OTP/phone/hour
• Aadhaar: never stored plaintext (DPDP Act)
"Make the boundary explicit. Make the contract clear.
Everything else is a detail." -- Uncle Bob