system-design48 min read

Design an Investment & Mutual Fund Platform — A Senior+ Guide | Ayodhyya

Design an Investment & Mutual Fund Platform

Building Groww, Zerodha Coin, and Kuvera at scale: NAV pipelines, SIP engines, KYC onboarding, and regulatory compliance

Senior+ Guide 50+ min read 10,000+ words Ayodhyya

1. Introduction — The Investment Platform Landscape

India's mutual fund industry has grown exponentially, with Assets Under Management (AUM) crossing ₹60 lakh crore in 2026. Digital investment platforms like Groww, Zerodha Coin, Kuvera, and Paytm Money have democratized access to mutual funds, SIPs, stocks, and ETFs for over 50 million retail investors. Building such a platform requires solving deeply complex challenges: real-time NAV (Net Asset Value) data pipelines, multi-exchange broker integration via BSE StAR MF and MFU, SEBI/AMFI regulatory compliance, KYC through CKYC and KRA, tax-optimized investing with ELSS, goal-based financial planning, and portfolio analytics that calculate XIRR, CAGR, and risk-adjusted returns.

A modern investment platform is not merely an e-commerce store for financial products. It is a mission-critical financial system where data accuracy is non-negotiable, regulatory audit trails must be immutable, transaction settlements must reconcile with exchange records within T+1 cycles, and customer money must never be commingled with operational funds. Unlike a typical SaaS application, an investment platform must handle sensitive financial data with bank-grade security, maintain complete audit logs for SEBI inspections, and ensure zero data loss even during peak market hours when millions of orders flow through the system.

Interview Context: Investment platform design questions test your understanding of financial domain modeling, real-time data pipelines, regulatory compliance systems, multi-party settlement workflows, and building systems where correctness trumps availability. This is one of the most domain-rich system design questions in fintech interviews.

The challenge of building such a platform spans multiple engineering disciplines simultaneously. You need distributed systems expertise for the high-throughput order management pipeline, data engineering skills for the NAV feed processing and reconciliation, security engineering for KYC and financial data protection, and front-end engineering for building responsive dashboards that display real-time portfolio valuations. The financial domain also introduces unique constraints such as T+1 settlement cycles, mandatory audit trails, two-factor authentication for transactions, and strict data residency requirements under Indian regulations.

This comprehensive guide walks through every major subsystem of an investment and mutual fund platform, from the initial data model and architecture to the intricate details of SIP scheduling, NAV reconciliation, brokerage integration, and client reporting. We will examine production-grade C# code examples, detailed database schemas, Mermaid architecture diagrams, and real-world capacity planning calculations. Whether you are preparing for a system design interview at a fintech company or actually building an investment platform, this guide provides the depth and breadth you need.

2. Investment Platform Landscape

The investment platform ecosystem in India involves multiple stakeholders connected through complex regulatory and technical relationships. Understanding this landscape is essential before designing any system component. The primary stakeholders include asset management companies (AMCs) that create and manage mutual fund schemes, registrar and transfer agents (RTAs) like CAMS and KFintech that maintain investor records, stock exchanges (BSE and NSE) that provide transaction platforms, the Mutual Fund Utilities (MFU) infrastructure, and SEBI as the overarching regulator.

Key Players and Their Roles

Entity Role Systems
AMCs (HDFC, SBI, ICICI) Create and manage mutual fund schemes, declare NAV daily Fund management systems, NAV calculation engines
RTAs (CAMS, KFintech) Maintain investor records, process transactions, dispatch statements Transfer agency systems, transaction processing
BSE StAR MF Electronic platform for mutual fund transactions Order routing, settlement, reconciliation
MFU (MF Utilities) Industry utility for transacting across all AMCs MFU Central, CAN (Common Account Number)
Depositories (CDSL, NSDL) Hold securities in dematerialized form Demat account management, settlement
KRA Agencies (CVL, CAMS, NDML) Maintain KYC records for investors CKYC database, KYC verification APIs
Platforms (Groww, Zerodha) Aggregate, simplify, and distribute mutual fund investments Front-end apps, order management, portfolio tracking

The transaction flow in the Indian mutual fund ecosystem follows a well-defined path. When an investor places an order through a platform, the platform routes it to either BSE StAR MF or MFU, which then communicates with the respective RTA. The RTA updates the investor's account and confirms the transaction back through the chain. For direct plans, the commission component is eliminated, resulting in lower expense ratios for the investor. The entire cycle from order placement to confirmation typically takes T+1 business days, although payment confirmation and unit allotment may follow different timelines depending on the payment mode and fund house policies.

Understanding these relationships is critical because each integration point represents a potential point of failure, a latency bottleneck, and a reconciliation challenge. A production investment platform must maintain resilient connections with all these systems while providing a seamless experience to the end user. The platform must also handle edge cases such as market holidays, NAV declaration delays, exchange connectivity issues, and regulatory circular changes that affect transaction processing.

3. Functional & Non-Functional Requirements

Functional Requirements

  • User registration with Aadhaar-based eKYC and PAN verification through CKYC
  • Browse and search mutual fund schemes across 40+ AMCs with filters by category, risk, returns, and fund size
  • Place lump sum investments and set up SIPs with daily, weekly, monthly, and quarterly frequencies
  • Redeem units partially or fully with instant redemption for amounts up to ₹50,000
  • Real-time portfolio tracking with current NAV, day change, overall returns, XIRR calculation
  • Goal-based investment planning with asset allocation recommendations based on time horizon
  • Watchlists for tracking schemes before investing
  • Download transaction history, capital gains statements, and tax certificates
  • Switch between schemes within the same AMC and trigger systematic transfer plans (STP)
  • Systematic withdrawal plans (SWP) for regular income from accumulated units
  • ELSS-specific views with 3-year lock-in tracking and Section 80C tax saving summaries
  • Multi-payment mode support: UPI, net banking, SIP auto-debit via NACH mandate
  • Push notifications for order confirmations, NAV updates, and SIP triggers
  • Admin panel for KYC approval queues, order management, and reconciliation oversight

Non-Functional Requirements

Requirement Target Rationale
Availability 99.95% (8.76 hrs downtime/year) Financial platform; users expect reliability during market hours
Latency (Read) p99 < 200ms for NAV/portfolio queries Real-time portfolio display requires fast reads
Latency (Write) p99 < 500ms for order placement Order confirmation must be near-instant
Throughput 10,000 orders/second peak Market open/close hours see order spikes
Data Durability Zero data loss (RPO = 0) Financial transactions cannot be lost; regulatory requirement
Consistency Strongly consistent for balances Investor unit balances must never be inconsistent
Security PCI-DSS adjacent, AES-256 at rest Financial data protection; regulatory compliance
Audit Trail Immutable append-only logs SEBI requires complete transaction audit trails
Design Principle: In financial platforms, consistency and durability always trump availability. A brief read-only mode during reconciliation is acceptable. An incorrect unit balance or a lost transaction is catastrophic and potentially a regulatory violation.

4. Capacity Estimation & Back-of-Envelope

User and Traffic Estimates

Assume a platform with 10 million registered users, 2 million monthly active users (MAU), and 500,000 daily active users (DAU). Approximately 100,000 transactions per day (investments, redemptions, switches). Peak hours during market open (9:15 AM to 11:00 AM) and close (2:30 PM to 3:30 PM) see 60% of daily traffic.

Metric Value Calculation
Total registered users 10 million Given
MAU 2 million 20% of registered
DAU 500,000 25% of MAU
Transactions/day 100,000 20% of DAU transact
Peak transactions/sec ~100 TPS sustained, 500 TPS spike 60% traffic in 4 hr peak window
NAV queries/sec ~2,000 QPS 500K DAU × 4 page views / 86,400 sec
Portfolio valuation queries/sec ~1,500 QPS Each active user checks portfolio ~3x/day

Storage Estimates

Data Type Record Size Daily Growth Annual Growth
User profiles 2 KB 5,000 new users 3.65 GB
Transactions 1.5 KB 100,000 54.75 GB
SIP records 0.5 KB 20,000 active SIPs modified 3.65 GB
NAV data 0.3 KB per scheme per day 2,000 schemes × 0.3 KB 219 MB
Portfolio snapshots 1 KB per user per day 500,000 DAU 182.5 GB
Audit logs 0.8 KB 500,000 events 146 GB

Bandwidth Estimates

Assuming the NAV feed from AMCs arrives as a batch file of 2,000 schemes at approximately 600 KB per daily update, the inbound bandwidth from NAV sources is negligible. The primary bandwidth consumer is serving portfolio pages to 500,000 DAU, where each page response averages 50 KB (HTML + JSON data), resulting in approximately 25 GB per day or roughly 290 KB/s average. During peak hours, this scales to approximately 725 KB/s. CDN caching of static assets and NAV-independent content can reduce origin bandwidth by 70-80%.

Cache Estimates

The NAV cache must hold approximately 2,000 schemes × 200 bytes per record = 400 KB, which easily fits in a single Redis instance. The portfolio cache for the top 100,000 most active users at 5 KB each requires approximately 500 MB. User session data for 50,000 concurrent sessions at 2 KB each requires approximately 100 MB. Total Redis memory requirement is approximately 1-2 GB, well within a single node's capacity with room for growth.

5. Data Model & Storage Schema

The data model for an investment platform must capture the complete lifecycle of an investor's journey, from registration and KYC through investment, holding, and redemption. The schema must support complex financial calculations such as unit averaging across multiple purchase lots, capital gains computation with FIFO (First In First Out) lot matching, and XIRR calculation requiring timestamped cash flows. We use PostgreSQL as the primary OLTP database for its strong ACID guarantees, JSONB support for flexible attributes, and mature ecosystem for financial applications.

Core Entity Relationships

erDiagram USER ||--o{ KYC_RECORD : has USER ||--o{ INVESTMENT_ACCOUNT : holds USER ||--o{ SIP : sets_up USER ||--o{ ORDER : places USER ||--o{ GOAL : defines INVESTMENT_ACCOUNT ||--o{ UNIT_HOLDING : contains INVESTMENT_ACCOUNT ||--o{ TRANSACTION : records ORDER ||--o{ ORDER_ITEM : contains ORDER_ITEM }o--|| SCHEME : invests_in SIP }o--|| SCHEME : invests_in SIP ||--o{ SIP_INSTALLMENT : generates SCHEME }o--|| AMC : managed_by SCHEME ||--|| FUND_CATEGORY : categorized_as SCHEME ||--o{ NAV_RECORD : has GOAL ||--o{ GOAL_ALLOCATION : allocates GOAL_ALLOCATION }o--|| SCHEME : invests_in

SQL Schema — Users and KYC

SQL
CREATE TABLE users (
    user_id         BIGSERIAL PRIMARY KEY,
    email           VARCHAR(255) UNIQUE NOT NULL,
    phone           VARCHAR(15) UNIQUE NOT NULL,
    full_name       VARCHAR(200) NOT NULL,
    pan_number      VARCHAR(10) UNIQUE,
    date_of_birth   DATE NOT NULL,
    status          VARCHAR(20) DEFAULT 'ACTIVE',
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE kyc_records (
    kyc_id          BIGSERIAL PRIMARY KEY,
    user_id         BIGINT REFERENCES users(user_id),
    ckyc_number     VARCHAR(20) UNIQUE,
    kra_reference   VARCHAR(30),
    pan_verified    BOOLEAN DEFAULT FALSE,
    aadhaar_verified BOOLEAN DEFAULT FALSE,
    address_proof   JSONB,
    verification_status VARCHAR(20) DEFAULT 'PENDING',
    verified_at     TIMESTAMPTZ,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE investment_accounts (
    account_id      BIGSERIAL PRIMARY KEY,
    user_id         BIGINT REFERENCES users(user_id),
    account_type    VARCHAR(30) NOT NULL,
    folio_number    VARCHAR(20),
    bse_code        VARCHAR(15),
    mfu_can         VARCHAR(20),
    status          VARCHAR(20) DEFAULT 'ACTIVE',
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

SQL Schema — Schemes and NAV

SQL
CREATE TABLE amcs (
    amc_id          SERIAL PRIMARY KEY,
    amc_code        VARCHAR(10) UNIQUE NOT NULL,
    amc_name        VARCHAR(200) NOT NULL,
    rta_code        VARCHAR(10),
    bse_code        VARCHAR(10),
    is_active       BOOLEAN DEFAULT TRUE
);

CREATE TABLE schemes (
    scheme_id       BIGSERIAL PRIMARY KEY,
    amc_id          INT REFERENCES amcs(amc_id),
    scheme_code     VARCHAR(20) UNIQUE NOT NULL,
    scheme_name     VARCHAR(500) NOT NULL,
    scheme_type     VARCHAR(30) NOT NULL,
    plan_type       VARCHAR(10) NOT NULL,
    option_type     VARCHAR(15) NOT NULL,
    category        VARCHAR(50),
    sub_category    VARCHAR(50),
    nav             DECIMAL(12,4),
    nav_date        DATE,
    expense_ratio   DECIMAL(5,4),
    aum             DECIMAL(18,2),
    risk_rating     VARCHAR(20),
    min_investment  DECIMAL(12,2),
    min_sip_amount  DECIMAL(12,2),
    is_active       BOOLEAN DEFAULT TRUE,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE nav_records (
    nav_id          BIGSERIAL PRIMARY KEY,
    scheme_id       BIGINT REFERENCES schemes(scheme_id),
    nav_value       DECIMAL(12,4) NOT NULL,
    nav_date        DATE NOT NULL,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(scheme_id, nav_date)
);

CREATE INDEX idx_nav_scheme_date ON nav_records(scheme_id, nav_date DESC);

SQL Schema — Transactions and Orders

SQL
CREATE TABLE orders (
    order_id        BIGSERIAL PRIMARY KEY,
    user_id         BIGINT REFERENCES users(user_id),
    order_type      VARCHAR(20) NOT NULL,
    order_status    VARCHAR(20) DEFAULT 'PENDING',
    exchange_ref    VARCHAR(30),
    bse_order_id    VARCHAR(20),
    total_amount    DECIMAL(14,2),
    payment_mode    VARCHAR(20),
    payment_ref     VARCHAR(50),
    placed_at       TIMESTAMPTZ DEFAULT NOW(),
    executed_at     TIMESTAMPTZ,
    settled_at      TIMESTAMPTZ
);

CREATE TABLE order_items (
    item_id         BIGSERIAL PRIMARY KEY,
    order_id        BIGINT REFERENCES orders(order_id),
    scheme_id       BIGINT REFERENCES schemes(scheme_id),
    transaction_type VARCHAR(10) NOT NULL,
    amount          DECIMAL(14,2),
    units           DECIMAL(14,4),
    nav             DECIMAL(12,4),
    folio_number    VARCHAR(20),
    status          VARCHAR(20) DEFAULT 'PENDING',
    bse_ref         VARCHAR(20)
);

CREATE TABLE transactions (
    txn_id          BIGSERIAL PRIMARY KEY,
    user_id         BIGINT REFERENCES users(user_id),
    account_id      BIGINT REFERENCES investment_accounts(account_id),
    scheme_id       BIGINT REFERENCES schemes(scheme_id),
    txn_type        VARCHAR(20) NOT NULL,
    amount          DECIMAL(14,2) NOT NULL,
    units           DECIMAL(14,4),
    nav             DECIMAL(12,4),
    nav_date        DATE,
    units_allotted  DECIMAL(14,4),
    stamp_duty      DECIMAL(10,2),
    stt             DECIMAL(10,2),
    exit_load       DECIMAL(10,2),
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

SQL Schema — SIP and Goals

SQL
CREATE TABLE sips (
    sip_id          BIGSERIAL PRIMARY KEY,
    user_id         BIGINT REFERENCES users(user_id),
    scheme_id       BIGINT REFERENCES schemes(scheme_id),
    account_id      BIGINT REFERENCES investment_accounts(account_id),
    amount          DECIMAL(12,2) NOT NULL,
    frequency       VARCHAR(15) NOT NULL,
    sip_date        INT NOT NULL,
    mandate_id      VARCHAR(20),
    status          VARCHAR(15) DEFAULT 'ACTIVE',
    start_date      DATE NOT NULL,
    end_date        DATE,
    total_installments INT DEFAULT 0,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE sip_installments (
    installment_id  BIGSERIAL PRIMARY KEY,
    sip_id          BIGINT REFERENCES sips(sip_id),
    installment_date DATE NOT NULL,
    amount          DECIMAL(12,2),
    nav             DECIMAL(12,4),
    units           DECIMAL(14,4),
    status          VARCHAR(15) DEFAULT 'SCHEDULED',
    retry_count     INT DEFAULT 0,
    executed_at     TIMESTAMPTZ
);

CREATE TABLE goals (
    goal_id         BIGSERIAL PRIMARY KEY,
    user_id         BIGINT REFERENCES users(user_id),
    goal_name       VARCHAR(200),
    target_amount   DECIMAL(16,2),
    current_value   DECIMAL(16,2) DEFAULT 0,
    target_date     DATE,
    risk_profile    VARCHAR(20),
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE goal_allocations (
    allocation_id   BIGSERIAL PRIMARY KEY,
    goal_id         BIGINT REFERENCES goals(goal_id),
    scheme_id       BIGINT REFERENCES schemes(scheme_id),
    allocated_pct   DECIMAL(5,2),
    invested_amount DECIMAL(14,2) DEFAULT 0
);
Design Note: The schema uses DECIMAL types for all monetary values to avoid floating-point precision issues that are unacceptable in financial systems. PostgreSQL's DECIMAL(14,2) supports amounts up to 999,999,999,999.99, sufficient for individual accounts. All timestamps use TIMESTAMPTZ for timezone-aware storage, critical for Indian financial markets operating in IST.

6. High-Level Architecture

The architecture follows a microservices pattern with clear domain boundaries. Each bounded context (User Management, KYC, Fund Catalog, Order Management, Portfolio, SIP Engine, NAV Pipeline, Reporting) operates as an independent service with its own database. Communication between services uses a combination of synchronous gRPC for latency-sensitive operations (order placement, NAV lookup) and asynchronous Kafka messaging for eventual consistency workflows (portfolio update after settlement, notification dispatch, audit logging).

graph TB subgraph "Client Layer" A[Web App - React] --> GW[API Gateway / BFF] B[Mobile App - React Native] --> GW end subgraph "Core Services" GW --> US[User Service] GW --> KS[KYC Service] GW --> FC[Fund Catalog Service] GW --> OM[Order Management] GW --> PE[Portfolio Engine] GW --> SE[SIP Engine] GW --> RE[Reporting Engine] end subgraph "Data Layer" US --> PG1[(PostgreSQL - Users)] FC --> PG2[(PostgreSQL - Funds)] OM --> PG3[(PostgreSQL - Orders)] PE --> PG4[(PostgreSQL - Holdings)] SE --> PG5[(PostgreSQL - SIPs)] end subgraph "Messaging" OM --> K1[Kafka - Order Events] K1 --> SE K1 --> PE K1 --> RE end subgraph "External Integrations" OM --> BSE[BSE StAR MF API] OM --> MFU[MFU API] FC --> NAV[NAV Feed Processor] KS --> CKYC[CKYC/KRA API] NAV --> S3[Object Storage - NAV Archive] end subgraph "Caching Layer" FC --> RD1[(Redis - NAV Cache)] PE --> RD2[(Redis - Portfolio Cache)] US --> RD3[(Redis - Session Cache)] end

Service Responsibilities

Service Responsibility Database
User Service Registration, authentication, profile management, MFA PostgreSQL (Users)
KYC Service CKYC/KRA integration, document verification, PAN validation PostgreSQL (KYC)
Fund Catalog Scheme master data, NAV display, search, filtering PostgreSQL (Funds) + Redis
Order Management Order lifecycle, exchange routing, payment processing PostgreSQL (Orders)
SIP Engine SIP scheduling, installment generation, mandate management PostgreSQL (SIPs)
Portfolio Engine Unit tracking, valuation, XIRR/CAGR calculation, gains PostgreSQL (Holdings) + Redis
NAV Pipeline NAV feed ingestion, validation, archival, broadcast PostgreSQL (NAV) + S3
Reporting Engine Capital gains statements, tax certificates, portfolio PDFs PostgreSQL + S3 (Reports)

7. API Design

All APIs follow RESTful conventions with JSON payloads. Authentication uses JWT tokens with 15-minute expiry and refresh token rotation. Financial write operations require a secondary OTP verification. Rate limiting is applied per-user with different tiers for read (1000/min), write (100/min), and financial (20/min) endpoints.

Key API Endpoints

HTTP
POST   /api/v1/auth/register
POST   /api/v1/auth/login
POST   /api/v1/auth/verify-otp
POST   /api/v1/kyc/initiate
GET    /api/v1/kyc/status/{userId}

GET    /api/v1/schemes?category=equity&risk=moderate&sort=returns_1y
GET    /api/v1/schemes/{schemeId}
GET    /api/v1/schemes/{schemeId}/nav-history?from=2025-01-01&to=2026-07-01
GET    /api/v1/schemes/search?q=hdfc+mid+cap

POST   /api/v1/orders/invest
POST   /api/v1/orders/redeem
POST   /api/v1/orders/switch
GET    /api/v1/orders/{orderId}
GET    /api/v1/orders?status=pending

POST   /api/v1/sips
GET    /api/v1/sips
PUT    /api/v1/sips/{sipId}
DELETE /api/v1/sips/{sipId}
GET    /api/v1/sips/{sipId}/installments

GET    /api/v1/portfolio/summary
GET    /api/v1/portfolio/holdings
GET    /api/v1/portfolio/returns
GET    /api/v1/portfolio/gains?financialYear=2025-26

POST   /api/v1/goals
GET    /api/v1/goals/{goalId}
PUT    /api/v1/goals/{goalId}/allocations

GET    /api/v1/reports/capital-gains?fy=2025-26
GET    /api/v1/reports/transactions?from=2025-04-01&to=2026-03-31
GET    /api/v1/reports/certificates/{certType}

Sample Request/Response — Place Investment Order

JSON
// POST /api/v1/orders/invest
{
    "schemeId": 12847,
    "amount": 5000.00,
    "paymentMode": "UPI",
    "upiId": "investor@upi",
    "clientOrderId": "ORD-2026-7-89234"
}

// Response 201 Created
{
    "orderId": "ORD-2026-7-89234",
    "status": "CONFIRMED",
    "schemeName": "HDFC Mid-Cap Opportunities Fund - Direct Growth",
    "amount": 5000.00,
    "estimatedUnits": null,
    "nav": null,
    "message": "Order placed. NAV will be applied at next declaration.",
    "paymentUrl": "upi://pay?pa=platform@upi&pn=MInvest&am=5000",
    "createdAt": "2026-07-13T09:45:00+05:30"
}

C# Order Service Implementation

C#
public class OrderService : IOrderService
{
    private readonly IOrderRepository _orderRepo;
    private readonly IBseClient _bseClient;
    private readonly IPaymentGateway _paymentGateway;
    private readonly IKafkaProducer _kafkaProducer;
    private readonly ILogger<OrderService> _logger;

    public OrderService(
        IOrderRepository orderRepo,
        IBseClient bseClient,
        IPaymentGateway paymentGateway,
        IKafkaProducer kafkaProducer,
        ILogger<OrderService> logger)
    {
        _orderRepo = orderRepo;
        _bseClient = bseClient;
        _paymentGateway = paymentGateway;
        _kafkaProducer = kafkaProducer;
        _logger = logger;
    }

    public async Task<OrderResult> PlaceInvestmentAsync(
        PlaceInvestmentRequest request, CancellationToken ct)
    {
        using var transaction = await _orderRepo.BeginTransactionAsync(ct);

        var order = new Order
        {
            UserId = request.UserId,
            OrderType = OrderType.Purchase,
            OrderStatus = OrderStatus.Pending,
            TotalAmount = request.Amount,
            PaymentMode = request.PaymentMode,
            PlacedAt = DateTime.UtcNow
        };

        var orderItem = new OrderItem
        {
            SchemeId = request.SchemeId,
            TransactionType = TransactionType.Purchase,
            Amount = request.Amount,
            Status = OrderItemStatus.Pending
        };
        order.Items.Add(orderItem);

        await _orderRepo.SaveOrderAsync(order, ct);

        var bseResponse = await _bseClient.PlaceOrderAsync(
            new BseOrderRequest
            {
                SchemeCode = request.SchemeCode,
                Amount = request.Amount,
                OrderType = BseOrderType.Purchase,
                ClientCode = request.ClientCode,
                ReferenceNumber = order.OrderId.ToString()
            }, ct);

        if (bseResponse.IsSuccess)
        {
            order.BseOrderId = bseResponse.BseOrderId;
            order.OrderStatus = OrderStatus.Confirmed;
            orderItem.BseRef = bseResponse.BseOrderId;
            orderItem.Status = OrderItemStatus.Confirmed;

            await _kafkaProducer.PublishAsync("order.confirmed",
                new OrderConfirmedEvent
                {
                    OrderId = order.OrderId,
                    UserId = order.UserId,
                    SchemeId = request.SchemeId,
                    Amount = request.Amount,
                    Timestamp = DateTime.UtcNow
                }, ct);
        }
        else
        {
            order.OrderStatus = OrderStatus.Failed;
            _logger.LogWarning(
                "BSE order failed: {Error}", bseResponse.ErrorMessage);
        }

        await _orderRepo.SaveChangesAsync(ct);
        await transaction.CommitAsync(ct);

        return new OrderResult
        {
            OrderId = order.OrderId.ToString(),
            Status = order.OrderStatus.ToString(),
            PaymentUrl = bseResponse.PaymentUrl
        };
    }
}

8. Fund Catalog & Search

The fund catalog service is the discovery layer of the platform, responsible for maintaining scheme master data from all AMCs, providing fast search and filtering, and serving the detailed scheme pages that investors use to make investment decisions. The catalog contains approximately 2,000 active mutual fund schemes across equity, debt, hybrid, solution-oriented, and other categories. Each scheme record carries rich metadata including performance metrics, expense ratios, fund manager details, portfolio holdings, and risk ratings.

Search Architecture

The search functionality uses Elasticsearch as the primary search engine, with PostgreSQL as the source of truth. Scheme data is synchronized from PostgreSQL to Elasticsearch via Debezium CDC (Change Data Capture), ensuring near-real-time index updates. Search queries support full-text search across scheme names and descriptions, faceted filtering by category, sub-category, AMC, risk rating, expense ratio range, and AUM range, sorting by returns (1Y, 3Y, 5Y), expense ratio, AUM, and recency, and typeahead suggestions for quick fund discovery.

Elasticsearch Index Schema

JSON
{
    "mappings": {
        "properties": {
            "schemeId": { "type": "long" },
            "schemeCode": { "type": "keyword" },
            "schemeName": { "type": "text", "analyzer": "english" },
            "amcName": { "type": "keyword" },
            "category": { "type": "keyword" },
            "subCategory": { "type": "keyword" },
            "planType": { "type": "keyword" },
            "optionType": { "type": "keyword" },
            "riskRating": { "type": "keyword" },
            "nav": { "type": "float" },
            "expenseRatio": { "type": "float" },
            "aum": { "type": "float" },
            "returns1Y": { "type": "float" },
            "returns3Y": { "type": "float" },
            "returns5Y": { "type": "float" },
            "minInvestment": { "type": "float" },
            "fundManager": { "type": "text" },
            "tags": { "type": "keyword" },
            "isELSS": { "type": "boolean" },
            "isActive": { "type": "boolean" },
            "updatedAt": { "type": "date" }
        }
    }
}

C# Search Service

C#
public class FundSearchService : IFundSearchService
{
    private readonly IElasticClient _elastic;
    private readonly IFundRepository _fundRepo;

    public async Task<PagedResult<SchemeDto>> SearchAsync(
        FundSearchRequest request, CancellationToken ct)
    {
        var searchDescriptor = new SearchDescriptor<SchemeDocument>()
            .Index("fund-catalog")
            .Size(request.PageSize)
            .From(request.Offset);

        searchDescriptor.Query(q => q
            .Bool(b => b
                .Must(mu => mu
                    .MultiMatch(mm => mm
                        .Fields(f => f
                            .Field(s => s.SchemeName, 3.0)
                            .Field(s => s.AmcName, 2.0)
                            .Field(s => s.Tags, 1.0))
                        .Query(request.Query)
                        .Type(TextQueryType.BestFields)))
                .Filter(fb => fb
                    .Term(t => t.Field(f => f.IsActive, true))
                    , fb => request.Category != null
                        ? fb.Term(t => t.Field(f => f.Category, request.Category))
                        : fb
                    , fb => request.RiskRating != null
                        ? fb.Term(t => t.Field(f => f.RiskRating, request.RiskRating))
                        : fb
                    , fb => request.MaxExpenseRatio.HasValue
                        ? fb.Range(r => r
                            .Field(f => f.ExpenseRatio)
                            .Lte(request.MaxExpenseRatio.Value))
                        : fb)));

        if (!string.IsNullOrEmpty(request.SortBy))
        {
            searchDescriptor.Sort(s => request.SortBy switch
            {
                "returns_1y" => s.Field(f => f.Field(d => d.Returns1Y).Order(SortOrder.Descending)),
                "expense_asc" => s.Field(f => f.Field(d => d.ExpenseRatio).Order(SortOrder.Ascending)),
                "aum_desc" => s.Field(f => f.Field(d => f.Aum).Order(SortOrder.Descending)),
                _ => s.Field(f => f.Field(d => d.UpdatedAt).Order(SortOrder.Descending))
            });
        }

        var response = await _elastic.SearchAsync<SchemeDocument>(searchDescriptor, ct);

        return new PagedResult<SchemeDto>
        {
            Items = response.Documents.Select(MapToDto).ToList(),
            Total = (int)response.Total,
            Page = request.Page,
            PageSize = request.PageSize
        };
    }
}

10. SIP (Systematic Investment Plan) Engine

The SIP Engine is one of the most critical subsystems in an investment platform. SIPs represent recurring investment commitments where investors automate periodic investments into mutual fund schemes. In India, monthly SIPs are the most popular, with SIP dates typically between 1st and 28th of each month. The engine must handle SIP registration, installment scheduling, NACH mandate-based auto-debit, failed payment retries, and NAV-based unit allotment. As of 2026, Indian mutual fund platforms collectively manage over 80 million active SIPs, with monthly inflows exceeding ₹20,000 crore.

SIP Processing Flow

flowchart TD A[SIP Registration] --> B[Create NACH Mandate] B --> C[Mandate Approved] C --> D[SIP Active in Scheduler] D --> E{Daily Scheduler Run} E --> F[Check SIP Due Dates] F --> G[Generate Installment Records] G --> H[Initiate NACH Debit] H --> I{Debit Status} I -->|Success| J[Place Order via BSE] J --> K[Order Confirmed] K --> L[Apply NAV on Allotment Date] L --> M[Credit Units to Holding] M --> N[Update Portfolio] I -->|Failure| O[Retry Logic] O --> P{Retry Count} P -->|< 3| H P -->|= 3| Q[Mark SIP Installment Failed] Q --> R[Notify Investor] R --> S[Update SIP Failure Count]

C# SIP Engine Implementation

C#
public class SipEngine : ISipEngine
{
    private readonly ISipRepository _sipRepo;
    private readonly INachClient _nachClient;
    private readonly IBseClient _bseClient;
    private readonly IKafkaProducer _kafka;
    private readonly ILogger<SipEngine> _logger;

    public async Task ProcessDailySipInstallmentsAsync(CancellationToken ct)
    {
        var today = DateOnly.FromDateTime(DateTime.Today);
        var dueSips = await _sipRepo.GetSipsDueForDateAsync(today, ct);

        _logger.LogInformation(
            "Processing {Count} SIP installments for {Date}",
            dueSips.Count, today);

        var tasks = dueSips.Select(sip => ProcessSipInstallmentAsync(sip, ct));
        await Task.WhenAll(tasks);
    }

    private async Task ProcessSipInstallmentAsync(
        Sip sip, CancellationToken ct)
    {
        try
        {
            var installment = new SipInstallment
            {
                SipId = sip.SipId,
                InstallmentDate = DateOnly.FromDateTime(DateTime.Today),
                Amount = sip.Amount,
                Status = SipInstallmentStatus.Processing
            };
            await _sipRepo.SaveInstallmentAsync(installment, ct);

            var debitResult = await _nachClient.InitiateDebitAsync(
                new NachDebitRequest
                {
                    MandateId = sip.MandateId,
                    Amount = sip.Amount,
                    ReferenceId = $"SIP-{sip.SipId}-{installment.InstallmentId}",
                    DebitDate = DateTime.Today
                }, ct);

            if (debitResult.IsSuccess)
            {
                var bseOrder = await _bseClient.PlaceSipOrderAsync(
                    new BseSipOrderRequest
                    {
                        SchemeCode = sip.SchemeCode,
                        Amount = sip.Amount,
                        ClientCode = sip.ClientCode,
                        SIPReference = sip.SipCode,
                        InstallmentNumber = sip.TotalInstallments + 1
                    }, ct);

                installment.Status = SipInstallmentStatus.Placed;
                installment.BseRef = bseOrder.ReferenceNumber;
                sip.TotalInstallments++;

                await _kafka.PublishAsync("sip.installment.placed",
                    new SipInstallmentEvent
                    {
                        SipId = sip.SipId,
                        InstallmentId = installment.InstallmentId,
                        Amount = sip.Amount,
                        Timestamp = DateTime.UtcNow
                    }, ct);
            }
            else
            {
                installment.Status = SipInstallmentStatus.Failed;
                installment.RetryCount++;
                sip.FailedInstallments++;

                if (sip.FailedInstallments >= 3)
                {
                    sip.Status = SipStatus.Suspended;
                    await _kafka.PublishAsync("sip.suspended",
                        new SipSuspendedEvent
                        {
                            SipId = sip.SipId,
                            Reason = "Multiple failed installments",
                            Timestamp = DateTime.UtcNow
                        }, ct);
                }
            }

            await _sipRepo.SaveChangesAsync(ct);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Error processing SIP installment for SIP {SipId}", sip.SipId);
            throw;
        }
    }
}

SIP Frequency Options

Frequency Processing Schedule Use Case
Monthly 1st - 28th of each month Most common; salary-based investing
Weekly Monday to Friday Aggressive accumulation strategy
Fortnightly 1st and 15th Bimonthly salary earners
Quarterly Selected quarter start dates Lump sum periodic investing
Annual Selected date once per year Bonus-based investing

11. Lump Sum Investment

Lump sum investment is the most straightforward transaction type where an investor makes a one-time investment of a specified amount into a mutual fund scheme. Unlike SIPs, there is no recurring commitment. The order is placed immediately, NAV is applied based on the cut-off time rules, and units are allotted accordingly. The SEBI-mandated cut-off time for equity schemes is 3:00 PM IST, and for liquid and overnight schemes, it is 1:30 PM IST. If payment is received before the cut-off, the same day's NAV is applied; otherwise, the next business day's NAV applies.

NAV Application Rules

Scheme Type Cut-off Time Settlement
Equity & Equity-Oriented Hybrid 3:00 PM IST T+1 units, T+2 money
Debt (except Liquid/Overnight) 3:00 PM IST T+1 units, T+2 money
Liquid & Overnight Funds 1:30 PM IST T+1 units, T+1 money
Instant Redemption (Liquid) Anytime (up to ₹50,000) Instant units, T+1 money to bank

C# Cut-off Time Validation

C#
public class CutoffTimeValidator : ICutoffTimeValidator
{
    public NAVApplicationResult ValidateNavApplication(
        SchemeType schemeType, DateTime orderTime, bool paymentReceived)
    {
        var istTime = TimeZoneInfo.ConvertTimeFromUtc(
            orderTime,
            TimeZoneInfo.FindSystemTimeZoneById("India Standard Time"));

        var cutoff = schemeType switch
        {
            SchemeType.Liquid or SchemeType.Overnight
                => new TimeSpan(13, 30, 0),
            _ => new TimeSpan(15, 0, 0)
        };

        bool sameDayNav = istTime.TimeOfDay <= cutoff && paymentReceived;

        return new NAVApplicationResult
        {
            SameDayNAV = sameDayNav,
            AppliedDate = sameDayNav
                ? DateOnly.FromDateTime(istTime)
                : DateOnly.FromDateTime(istTime).AddDays(1),
            CutOffTime = cutoff,
            OrderTime = istTime,
            Message = sameDayNav
                ? "Order qualifies for same-day NAV"
                : "Order will be processed with next business day NAV"
        };
    }
}

12. Redemption & Withdrawal

Redemption is the process of selling mutual fund units back to the AMC to receive cash. The redemption proceeds are typically credited to the investor's registered bank account within T+1 to T+3 business days depending on the scheme type. Equity scheme redemptions settle in T+3, debt schemes in T+1 to T+2, and liquid schemes in T+1. The platform must calculate applicable exit loads, stamp duty, and capital gains tax before confirming the redemption amount. For equity schemes held for less than one year, short-term capital gains tax of 20% applies. For debt schemes, gains are taxed at the investor's income tax slab rate.

Exit Load Calculation

C#
public class ExitLoadCalculator
{
    public RedemptionCalculation CalculateRedemption(
        List<PurchaseLot> lots, decimal redeemUnits, Scheme scheme)
    {
        var sortedLots = lots.OrderBy(l => l.PurchaseDate).ToList();
        decimal unitsRemaining = redeemUnits;
        decimal totalProceeds = 0;
        decimal totalExitLoad = 0;
        decimal totalCapitalGain = 0;
        var today = DateOnly.FromDateTime(DateTime.Today);

        foreach (var lot in sortedLots)
        {
            if (unitsRemaining <= 0) break;

            decimal unitsFromLot = Math.Min(unitsRemaining, lot.AvailableUnits);
            decimal lotProceeds = unitsFromLot * lot.CurrentNAV;
            decimal holdingDays = (today.DayNumber - lot.PurchaseDate.DayNumber);

            decimal exitLoadPct = CalculateExitLoadPercentage(
                scheme.Category, scheme.ExitLoadStructure, holdingDays);
            decimal lotExitLoad = lotProceeds * exitLoadPct / 100;

            decimal lotGain = lotProceeds - (unitsFromLot * lot.PurchaseNAV);
            bool isSTTApplicable = scheme.Category.Contains("Equity") &&
                                   holdingDays < 365;

            totalProceeds += lotProceeds;
            totalExitLoad += lotExitLoad;
            totalCapitalGain += lotGain;
            unitsRemaining -= unitsFromLot;
        }

        return new RedemptionCalculation
        {
            GrossAmount = totalProceeds,
            ExitLoad = totalExitLoad,
            NetAmount = totalProceeds - totalExitLoad,
            CapitalGain = totalCapitalGain,
            StampDuty = totalProceeds * 0.005m,
            EstimatedCreditDays = GetSettlementDays(scheme.Category)
        };
    }
}

13. Portfolio Tracking & Analytics

Portfolio tracking is the core value proposition of any investment platform. Investors need to see their current holdings, day-wise P&L, overall returns in absolute and percentage terms, XIRR (Extended Internal Rate of Return) for time-weighted return measurement, and detailed transaction history. The portfolio engine must aggregate data from multiple sources: current unit holdings (updated after each transaction settlement), daily NAV updates for real-time valuation, historical NAV data for return calculations, and cost basis data for capital gains computation.

XIRR Calculation

XIRR is the gold standard for measuring investment returns because it accounts for the timing and magnitude of all cash flows. Unlike simple CAGR which assumes a single lump sum investment at the start, XIRR handles multiple investments at different times (SIPs), partial redemptions, and switches. The calculation uses Newton-Raphson iteration to find the discount rate that makes the net present value of all cash flows equal to zero.

C#
public class XirrCalculator
{
    public double CalculateXIRR(
        List<CashFlow> cashFlows, double initialGuess = 0.1,
        int maxIterations = 100, double tolerance = 1e-7)
    {
        double rate = initialGuess;

        for (int i = 0; i < maxIterations; i++)
        {
            double npv = 0;
            double dnpv = 0;

            var baseDate = cashFlows.Min(c => c.Date);

            foreach (var cf in cashFlows)
            {
                double years = (cf.Date - baseDate).TotalDays / 365.25;
                double factor = Math.Pow(1 + rate, years);
                npv += cf.Amount / factor;
                dnpv -= years * cf.Amount / (factor * (1 + rate));
            }

            if (Math.Abs(npv) < tolerance)
                return rate;

            rate -= npv / dnpv;
        }

        throw new ConvergenceException(
            "XIRR calculation did not converge after " +
            $"{maxIterations} iterations");
    }
}

public record CashFlow(DateTime Date, decimal Amount);

Portfolio Summary Response

JSON
{
    "portfolioSummary": {
        "totalInvested": 485000.00,
        "currentValue": 612340.50,
        "totalGain": 127340.50,
        "totalGainPercent": 26.25,
        "xirr": 18.72,
        "dayChange": 1245.30,
        "dayChangePercent": 0.20,
        "totalSIPsActive": 8,
        "monthlySIPOutflow": 25000.00
    },
    "holdings": [
        {
            "schemeId": 12847,
            "schemeName": "HDFC Mid-Cap Opportunities Fund",
            "category": "Equity - Mid Cap",
            "units": 523.4567,
            "nav": 142.67,
            "currentValue": 74681.22,
            "investedAmount": 60000.00,
            "gain": 14681.22,
            "gainPercent": 24.47,
            "xirr": 21.35,
            "holdingDays": 487
        }
    ]
}

14. Goal-Based Investing

Goal-based investing transforms passive portfolio management into active financial planning. Instead of just buying funds, investors define life goals — children's education, retirement, house purchase, emergency fund — and the platform allocates investments across schemes to optimize for the target amount within the time horizon. The system calculates required monthly SIP amounts using future value of annuity formulas, recommends asset allocation based on the goal's time horizon and risk tolerance, and tracks progress with projections showing whether the investor is on track.

Asset Allocation by Time Horizon

Goal Time Horizon Equity % Debt % Gold % Risk Profile
Less than 2 years 10% 80% 10% Conservative
2 - 5 years 40% 45% 15% Moderate
5 - 10 years 65% 25% 10% Growth
10 - 15 years 80% 15% 5% Aggressive Growth
More than 15 years 90% 5% 5% Aggressive

C# Goal Projection Service

C#
public class GoalProjectionService
{
    public GoalProjection CalculateProjection(Goal goal)
    {
        var monthsRemaining = ((goal.TargetDate.Year -
            DateTime.Today.Year) * 12) +
            goal.TargetDate.Month - DateTime.Today.Month;

        var monthlyInflationRate = 0.06m / 12;
        var futureTarget = goal.TargetAmount *
            Math.Pow(1 + (double)monthlyInflationRate, monthsRemaining);

        var expectedReturn = GetExpectedReturn(goal.RiskProfile) / 100m;
        var monthlyReturn = expectedReturn / 12;

        var requiredSIP = monthlyReturn > 0
            ? (decimal)((double)futureTarget *
                ((double)monthlyReturn /
                (Math.Pow(1 + (double)monthlyReturn, monthsRemaining) - 1)))
            : futureTarget / monthsRemaining;

        var projectedValue = CalculateFV(
            goal.CurrentValue, requiredSIP,
            monthlyReturn, monthsRemaining);

        return new GoalProjection
        {
            GoalId = goal.GoalId,
            GoalName = goal.GoalName,
            TargetAmount = goal.TargetAmount,
            InflationAdjustedTarget = (decimal)futureTarget,
            RequiredMonthlySIP = Math.Ceiling(requiredSIP / 500) * 500,
            CurrentValue = goal.CurrentValue,
            ProjectedValue = (decimal)projectedValue,
            OnTrack = projectedValue >= (double)futureTarget,
            CompletionPercent = (decimal)(
                (double)goal.CurrentValue /
                (double)futureTarget * 100),
            MonthsRemaining = monthsRemaining
        };
    }
}

15. Tax Harvesting & ELSS

Tax optimization is a significant value-add for investors. Equity-linked savings schemes (ELSS) offer Section 80C tax deductions up to ₹1.5 lakh per financial year with the shortest lock-in period of 3 years among all 80C instruments. Beyond ELSS, the platform should implement tax-loss harvesting — strategically redeeming loss-making equity investments to offset capital gains and reduce tax liability. The system must maintain detailed lot-level tracking with FIFO matching to compute exact short-term and long-term capital gains for each financial year.

Capital Gains Tax Rates (FY 2025-26)

Asset Type Holding Period Tax Type Tax Rate
Equity & Equity-Oriented Funds ≤ 12 months Short-Term Capital Gains (STCG) 20%
Equity & Equity-Oriented Funds > 12 months Long-Term Capital Gains (LTCG) 12.5% above ₹1.25 lakh
Debt Funds Any As per income tax slab Slab rate
ELSS Funds ≤ 3 years (lock-in) STCG (after lock-in) 20%
ELSS Funds > 3 years LTCG 12.5% above ₹1.25 lakh

Tax-Loss Harvesting Logic

C#
public class TaxLossHarvestingService
{
    public TaxHarvestingReport AnalyzeHarvestOpportunities(
        string userId, int financialYear)
    {
        var holdings = GetHoldings(userId);
        var stcgPositions = new List<GainPosition>();
        var ltcgPositions = new List<GainPosition>();
        var harvestableLosses = new List<HarvestCandidate>();

        foreach (var holding in holdings)
        {
            foreach (var lot in holding.PurchaseLots)
            {
                var gain = lot.CurrentValue - lot.InvestedAmount;
                var holdingDays = (DateTime.Today - lot.PurchaseDate).Days;

                var position = new GainPosition
                {
                    SchemeId = holding.SchemeId,
                    SchemeName = holding.SchemeName,
                    Gain = gain,
                    HoldingDays = holdingDays,
                    PurchaseDate = lot.PurchaseDate,
                    Units = lot.Units
                };

                if (holdingDays <= 365)
                    stcgPositions.Add(position);
                else
                    ltcgPositions.Add(position);

                if (gain < 0 && holdingDays > 365)
                {
                    harvestableLosses.Add(new HarvestCandidate
                    {
                        SchemeId = holding.SchemeId,
                        SchemeName = holding.SchemeName,
                        LossAmount = Math.Abs(gain),
                        Units = lot.Units,
                        HoldingDays = holdingDays,
                        PotentialTaxSaving = Math.Abs(gain) * 0.125m
                    });
                }
            }
        }

        var totalSTCG = stcgPositions.Sum(p => p.Gain);
        var totalLTCG = ltcgPositions.Sum(p => p.Gain);
        var taxableLTCG = Math.Max(0, totalLTCG - 125000m);
        var estimatedTax = totalSTCG * 0.20m + taxableLTCG * 0.125m;

        return new TaxHarvestingReport
        {
            FinancialYear = financialYear,
            TotalSTCG = totalSTCG,
            TotalLTCG = totalLTCG,
            TaxableLTCG = taxableLTCG,
            EstimatedTax = estimatedTax,
            HarvestableLosses = harvestableLosses
                .OrderByDescending(h => h.PotentialTaxSaving).ToList(),
            PotentialTotalSaving = harvestableLosses
                .Sum(h => h.PotentialTaxSaving)
        };
    }
}

16. KYC & Onboarding (CKYC/KRA)

Know Your Customer (KYC) is a mandatory regulatory requirement for all financial investments in India. Every investor must complete KYC verification before making any investment. The platform integrates with Central KYC (CKYC) maintained by CERSAI, and KYC Registration Agencies (KRAs) like CVL KRA, CAMS KRA, and NDML KRA. The onboarding flow includes PAN verification, Aadhaar-based eKYC through UIDAI (DigiLocker integration), bank account verification via penny drop, and risk profiling questionnaire.

Onboarding Flow

flowchart TD A[User Registers] --> B[PAN Verification] B --> C{PAN Valid?} C -->|Yes| D[Aadhaar eKYC via DigiLocker] C -->|No| E[Reject - Invalid PAN] D --> F[CKYC Number Check] F --> G{Existing CKYC?} G -->|Yes| H[Fetch KYC Data from CKYC] G -->|No| I[Full KYC - Upload Documents] H --> J[Bank Account Verification] I --> J J --> K[Penny Drop - Re ₹1] K --> L{Bank Verified?} L -->|Yes| M[Risk Profiling Questionnaire] L -->|No| N[Retry with Different Account] M --> O[KYC Complete - Account Active]

C# KYC Verification Service

C#
public class KycVerificationService : IKycService
{
    private readonly ICkycClient _ckycClient;
    private readonly IKraClient _kraClient;
    private readonly IPennyDropClient _pennyDropClient;
    private readonly IKycRepository _kycRepo;

    public async Task<KycResult> InitiateKycAsync(
        KycRequest request, CancellationToken ct)
    {
        var kycRecord = new KycRecord
        {
            UserId = request.UserId,
            PanNumber = request.PanNumber,
            CreatedAt = DateTime.UtcNow
        };

        var panVerification = await VerifyPanAsync(
            request.PanNumber, request.FullName, request.DateOfBirth, ct);
        if (!panVerification.IsValid)
        {
            return KycResult.Failure("PAN verification failed: " +
                panVerification.ErrorMessage);
        }
        kycRecord.PanVerified = true;

        var ckycResult = await _ckycClient.SearchByPanAsync(
            request.PanNumber, ct);
        if (ckycResult.Found)
        {
            kycRecord.CkycNumber = ckycResult.CkycNumber;
            kycRecord.VerificationStatus = KycStatus.Verified;
            kycRecord.VerifiedAt = DateTime.UtcNow;
        }
        else
        {
            kycRecord.VerificationStatus = KycStatus.Pending;
        }

        await _kycRepo.SaveKycRecordAsync(kycRecord, ct);

        return KycResult.Success(kycRecord);
    }

    public async Task<BankVerificationResult> VerifyBankAccountAsync(
        string userId, string ifsc, string accountNumber, CancellationToken ct)
    {
        var pennyDropResult = await _pennyDropClient.VerifyAsync(
            new PennyDropRequest
            {
                IfscCode = ifsc,
                AccountNumber = accountNumber,
                AccountHolderName = "To be verified"
            }, ct);

        if (pennyDropResult.Status == PennyDropStatus.Verified)
        {
            await _kycRepo.UpdateBankVerificationAsync(
                userId, ifsc, accountNumber,
                pennyDropResult.AccountHolderName, ct);

            return BankVerificationResult.Success(
                pennyDropResult.AccountHolderName);
        }

        return BankVerificationResult.Failure(
            pennyDropResult.ErrorMessage);
    }
}

17. Order Management System

The Order Management System (OMS) is the central nervous system of the investment platform. It manages the complete lifecycle of every investment order from placement through execution, settlement, and reconciliation. The OMS must handle multiple order types (purchase, redemption, switch, STP, SWP), route orders to the appropriate exchange or MFU, manage payment processing, handle order modifications and cancellations, and maintain a complete audit trail. The system is designed with event sourcing principles to ensure every state change is recorded and auditable.

Order State Machine

stateDiagram-v2 [*] --> Placed Placed --> PaymentPending : Awaiting payment Placed --> Rejected : Validation failed PaymentPending --> Paid : Payment confirmed PaymentPending --> Cancelled : Payment timeout Paid --> SubmittedToExchange : Sent to BSE/MFU SubmittedToExchange --> Acknowledged : Exchange ACK SubmittedToExchange --> ExchangeRejected : Exchange NACK Acknowledged --> Executed : NAV applied Executed --> Settled : Units credited T+1 Settled --> Completed : Reconciled ExchangeRejected --> RefundPending : Auto-refund RefundPending --> Refunded : Money returned Cancelled --> RefundPending : Refund initiated

C# Order State Manager

C#
public class OrderStateMachine
{
    private static readonly Dictionary<OrderStatus, HashSet<OrderStatus>>
        _validTransitions = new()
    {
        [OrderStatus.Placed] = new()
            { OrderStatus.PaymentPending, OrderStatus.Rejected },
        [OrderStatus.PaymentPending] = new()
            { OrderStatus.Paid, OrderStatus.Cancelled },
        [OrderStatus.Paid] = new()
            { OrderStatus.SubmittedToExchange },
        [OrderStatus.SubmittedToExchange] = new()
            { OrderStatus.Acknowledged, OrderStatus.ExchangeRejected },
        [OrderStatus.Acknowledged] = new()
            { OrderStatus.Executed },
        [OrderStatus.Executed] = new()
            { OrderStatus.Settled },
        [OrderStatus.Settled] = new()
            { OrderStatus.Completed },
        [OrderStatus.ExchangeRejected] = new()
            { OrderStatus.RefundPending },
        [OrderStatus.Cancelled] = new()
            { OrderStatus.RefundPending },
        [OrderStatus.RefundPending] = new()
            { OrderStatus.Refunded }
    };

    public static void Transition(Order order, OrderStatus newStatus)
    {
        if (!_validTransitions.ContainsKey(order.OrderStatus) ||
            !_validTransitions[order.OrderStatus].Contains(newStatus))
        {
            throw new InvalidOrderTransitionException(
                $"Cannot transition from {order.OrderStatus} to {newStatus}");
        }

        var previousStatus = order.OrderStatus;
        order.OrderStatus = newStatus;
        order.StatusHistory.Add(new OrderStatusChange
        {
            FromStatus = previousStatus,
            ToStatus = newStatus,
            ChangedAt = DateTime.UtcNow
        });
    }
}

18. Brokerage Integration (BSE/NSE/MFU)

The platform integrates with multiple exchange platforms to execute mutual fund transactions. BSE StAR MF (Stock Exchange Application for Trading - Mutual Funds) is the primary integration point, handling the majority of mutual fund transactions in India. MFU (Mutual Fund Utilities) provides an alternative channel that connects to all AMCs through a single interface. The integration involves multiple API endpoints for order placement, order status tracking, mandate registration, and transaction inquiry.

Integration Architecture

sequenceDiagram participant Platform participant OMS participant BSE as BSE StAR MF participant RTA as CAMS/KFintech participant Bank as Payment Gateway Platform->>OMS: Place Order OMS->>OMS: Validate & Create Order OMS->>Bank: Initiate Payment Bank-->>OMS: Payment Confirmed OMS->>BSE: Submit Order (SOAP/REST) BSE-->>OMS: Order Acknowledgment OMS->>OMS: Update Status: Acknowledged BSE->>RTA: Forward to RTA RTA->>RTA: Process & Allot Units RTA-->>BSE: Transaction Confirmation BSE-->>OMS: Status Update: Executed OMS->>OMS: Update Units & Status OMS-->>Platform: Order Completed

BSE StAR MF API Integration

C#
public class BseStarMfClient : IBseClient
{
    private readonly HttpClient _httpClient;
    private readonly BseConfig _config;

    public async Task<BseResponse> PlaceOrderAsync(
        BseOrderRequest request, CancellationToken ct)
    {
        var soapEnvelope = BuildOrderEnvelope(request);
        var content = new StringContent(
            soapEnvelope, Encoding.UTF8, "text/xml");
        content.Headers.Add("SOAPAction",
            "http://bsestarmf.in/OrderEntry");

        var response = await _httpClient.PostAsync(
            _config.OrderEndpoint, content, ct);
        var xml = await response.Content.ReadAsStringAsync(ct);

        var doc = XDocument.Parse(xml);
        var status = doc.Root?
            .Element("{http://bsestarmf.in}Body")?
            .Element("{http://bsestarmf.in}OrderEntryResponse")?
            .Element("returnCode")?.Value;

        if (status == "0")
        {
            return BseResponse.Success(
                doc.Root.Element("{http://bsestarmf.in}Body")
                    .Element("{http://bsestarmf.in}OrderEntryResponse")
                    .Element("orderNumber")?.Value);
        }

        return BseResponse.Failure(
            doc.Root.Element("{http://bsestarmf.in}Body")
                .Element("{http://bsestarmf.in}OrderEntryResponse")
                .Element("returnDescription")?.Value);
    }

    public async Task<BseOrderStatus> GetOrderStatusAsync(
        string bseOrderId, CancellationToken ct)
    {
        var response = await _httpClient.GetAsync(
            $"{_config.StatusEndpoint}?orderNo={bseOrderId}", ct);
        var xml = await response.Content.ReadAsStringAsync(ct);
        return ParseOrderStatus(xml);
    }

    private string BuildOrderEnvelope(BseOrderRequest request)
    {
        return $@"<soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
            <soap:Body>
                <OrderEntry xmlns=""http://bsestarmf.in"">
                    <password>{_config.Password}</password>
                    <userId>{_config.UserId}</userId>
                    <clientCode>{request.ClientCode}</clientCode>
                    <schemeCd>{request.SchemeCode}</schemeCd>
                    <buySell>P</buySell>
                    <buySellType>FRESH</buySellType>
                    <dpTxnMode>P</dpTxnMode>
                    <orderVal>{request.Amount}</orderVal>
                    <allRedeem>F</allRedeem>
                    <units>0</units>
                    <fyPause>N</fyPause>
                </OrderEntry>
            </soap:Body>
        </soap:Envelope>";
    }
}

Exchange Comparison

Feature BSE StAR MF MFU NSE NMF
Market Share ~55% of MF transactions ~25% ~20%
Protocol SOAP/XML + REST SOAP/XML REST
Settlement T+1 via exchange Direct with RTA T+1 via exchange
AMC Coverage All SEBI-registered AMCs All AMCs Most AMCs
Direct Plans Yes Yes Yes

20. Client Reporting

Client reporting encompasses all documents and statements that investors need for record-keeping, tax filing, and financial planning. The platform must generate capital gains statements with lot-level detail, tax saving certificates for ELSS investments, annual consolidated portfolio statements, transaction history exports, and Form 16A/16B equivalents. Reports must be generated on-demand and also scheduled for periodic delivery via email. For tax purposes, reports must be organized by financial year (April to March) and provide both summary and detailed views.

Report Types

Report Content Format Trigger
Capital Gains Statement STCG, LTCG with lot details per scheme PDF, CSV On-demand, year-end
Transaction Statement All transactions with dates, NAVs, amounts PDF, CSV On-demand, monthly
Portfolio Valuation Current holdings, NAV, value, P&L PDF On-demand, daily
Tax Saving Certificate ELSS investments for 80C proof PDF On-demand, year-end
Investment Proof All investments for employer verification PDF On-demand
SIP Summary Active SIPs, installments, total invested PDF, CSV On-demand, monthly

C# Report Generation Service

C#
public class ReportGenerationService
{
    public async Task<byte[]> GenerateCapitalGainsReportAsync(
        string userId, int financialYear, CancellationToken ct)
    {
        var transactions = await GetTransactionsForFYAsync(
            userId, financialYear, ct);
        var currentNavs = await GetCurrentNavsAsync(ct);

        var lotTracker = new FIFOLotTracker();
        var gains = new List<CapitalGainEntry>();

        foreach (var txn in transactions.OrderBy(t => t.Date))
        {
            switch (txn.Type)
            {
                case TransactionType.Purchase:
                    lotTracker.AddLot(new PurchaseLot
                    {
                        SchemeId = txn.SchemeId,
                        Units = txn.Units,
                        NAV = txn.NAV,
                        Date = txn.Date,
                        Amount = txn.Amount
                    });
                    break;

                case TransactionType.Redeem:
                    var matchedLots = lotTracker.MatchLotsFIFO(
                        txn.SchemeId, txn.Units);
                    foreach (var lot in matchedLots)
                    {
                        var currentNav = currentNavs[txn.SchemeId];
                        var holdingDays = (txn.Date - lot.Date).Days;
                        var gain = (txn.NAV - lot.NAV) * lot.MatchedUnits;
                        var isLongTerm = holdingDays > 365;

                        gains.Add(new CapitalGainEntry
                        {
                            SchemeId = txn.SchemeId,
                            SchemeName = txn.SchemeName,
                            PurchaseDate = lot.Date,
                            SaleDate = txn.Date,
                            HoldingDays = holdingDays,
                            Units = lot.MatchedUnits,
                            PurchaseNAV = lot.NAV,
                            SaleNAV = txn.NAV,
                            GainAmount = gain,
                            GainType = isLongTerm
                                ? GainType.LTCG : GainType.STCG,
                            TaxRate = isLongTerm ? 12.5m : 20m
                        });
                    }
                    break;
            }
        }

        return GeneratePdf(gains, financialYear);
    }
}

21. Risk Profiling

Risk profiling is the process of assessing an investor's risk appetite based on their financial situation, investment experience, time horizon, and behavioral tendencies. SEBI mandates that every distributor must assess the risk profile of investors and recommend schemes that match their risk tolerance. The risk profiling questionnaire typically covers age, income, investment experience, loss tolerance, investment horizon, and liquidity needs. The responses are scored to classify investors into risk categories from conservative to aggressive.

Risk Category Mapping

Risk Score Category Recommended Allocation
0 - 20 Conservative 80% Debt, 15% Balanced Hybrid, 5% Equity
21 - 40 Moderately Conservative 60% Debt, 25% Balanced Hybrid, 15% Equity
41 - 60 Moderate 40% Debt, 20% Hybrid, 40% Equity
61 - 80 Moderately Aggressive 20% Debt, 10% Hybrid, 70% Equity
81 - 100 Aggressive 5% Debt, 5% Hybrid, 90% Equity

C# Risk Scoring Engine

C#
public class RiskProfilingEngine
{
    private static readonly Dictionary<string, int[]> _scoringMatrix = new()
    {
        ["age"] = { 10, 8, 6, 4, 2 },
        ["income"] = { 2, 4, 6, 8, 10 },
        ["experience"] = { 1, 3, 5, 7, 9 },
        ["lossTolerance"] = { 1, 3, 5, 7, 10 },
        ["horizon"] = { 2, 4, 6, 8, 10 },
        ["liquidityNeeds"] = { 10, 8, 6, 4, 2 }
    };

    public RiskProfile CalculateRiskProfile(
        RiskQuestionnaireAnswers answers)
    {
        var scores = new Dictionary<string, int>
        {
            ["age"] = _scoringMatrix["age"][MapAgeRange(answers.Age)],
            ["income"] = _scoringMatrix["income"][MapIncomeRange(answers.AnnualIncome)],
            ["experience"] = _scoringMatrix["experience"][answers.InvestmentExperience],
            ["lossTolerance"] = _scoringMatrix["lossTolerance"][answers.LossTolerance],
            ["horizon"] = _scoringMatrix["horizon"][MapHorizonRange(answers.InvestmentHorizon)],
            ["liquidityNeeds"] = _scoringMatrix["liquidityNeeds"][answers.LiquidityNeeds]
        };

        var totalScore = scores.Values.Sum();
        var maxScore = scores.Count * 10;
        var normalizedScore = (double)totalScore / maxScore * 100;

        var category = normalizedScore switch
        {
            <= 20 => RiskCategory.Conservative,
            <= 40 => RiskCategory.ModeratelyConservative,
            <= 60 => RiskCategory.Moderate,
            <= 80 => RiskCategory.ModeratelyAggressive,
            _ => RiskCategory.Aggressive
        };

        return new RiskProfile
        {
            TotalScore = totalScore,
            NormalizedScore = normalizedScore,
            Category = category,
            DimensionScores = scores,
            RecommendedAllocation = GetAllocation(category),
            ValidUntil = DateTime.UtcNow.AddMonths(12)
        };
    }
}

22. Recommendation Engine

The recommendation engine combines the investor's risk profile, investment goals, time horizon, tax situation, and market conditions to suggest suitable mutual fund schemes. Recommendations must comply with SEBI guidelines on suitability — each recommendation must be backed by a documented rationale linking the investor's profile to the scheme's characteristics. The engine uses a rule-based approach augmented with collaborative filtering to identify schemes that similar investors have found successful.

Recommendation Algorithm

flowchart TD A[Investor Profile] --> B{Risk Category} B --> C[Fetch Candidate Schemes] C --> D[Filter by Category Match] D --> E[Filter by Expense Ratio] E --> F[Rank by Risk-Adjusted Returns] F --> G[Apply Diversification Rules] G --> H[Check Suitability Matrix] H --> I[Generate Recommendation Set] I --> J[Apply Concentration Limits] J --> K[Final Recommendations]

C# Recommendation Engine

C#
public class RecommendationEngine
{
    public async Task<RecommendationSet> GenerateRecommendationsAsync(
        InvestorProfile profile, CancellationToken ct)
    {
        var candidateSchemes = await GetCandidateSchemesAsync(
            profile.RiskProfile.Category, ct);

        var scored = candidateSchemes
            .Select(s => new ScoredScheme
            {
                Scheme = s,
                Score = CalculateRecommendationScore(s, profile)
            })
            .OrderByDescending(x => x.Score)
            .ToList();

        var recommendations = new List<SchemeRecommendation>();
        decimal allocatedPct = 0;

        foreach (var scored_scheme in scored)
        {
            if (allocatedPct >= 100) break;
            if (recommendations.Count >= 8) break;

            var allocation = CalculateAllocation(
                scored_scheme.Scheme, profile);

            if (allocatedPct + allocation.Percentage > 100)
                allocation.Percentage = 100 - allocatedPct;

            if (allocation.Percentage >= 5)
            {
                recommendations.Add(new SchemeRecommendation
                {
                    SchemeId = scored_scheme.Scheme.SchemeId,
                    SchemeName = scored_scheme.Scheme.SchemeName,
                    AllocationPercentage = allocation.Percentage,
                    Rationale = GenerateRationale(
                        scored_scheme.Scheme, profile),
                    RiskMatch = scored_scheme.Score.RiskMatch,
                    ReturnScore = scored_scheme.Score.ReturnScore
                });
                allocatedPct += allocation.Percentage;
            }
        }

        return new RecommendationSet
        {
            InvestorId = profile.UserId,
            GeneratedAt = DateTime.UtcNow,
            Recommendations = recommendations,
            RebalanceFrequency = "Quarterly",
            Disclaimer = "Past performance is not indicative of future results."
        };
    }
}

23. Regulatory Compliance (SEBI/AMFI)

Regulatory compliance is the backbone of every operation in an investment platform. SEBI (Securities and Exchange Board of India) and AMFI (Association of Mutual Funds in India) impose extensive regulations that govern how platforms operate, what disclosures they must make, how they handle client money, and what audit trails they must maintain. Key regulations include SEBI's Mutual Fund Regulations 1996 (amended), SEBI (Intermediaries) Regulations 2008, KYC norms under PMLA, and various circulars on transaction processing, disclosure requirements, and investor protection.

Key Compliance Requirements

Regulation Requirement Implementation
SEBI KYC Norms Mandatory KYC for all investors before transaction CKYC/KRA integration at onboarding
Transaction Audit Trail Complete, immutable record of all transactions Append-only audit log table with checksums
Client Money Rules Client funds must not be mixed with operational funds Segregated bank accounts, daily reconciliation
NAV Disclosure NAV must be declared daily by 11 PM Automated feed monitoring with alert escalation
Expense Ratio Limits Maximum TER as per SEBI guidelines Validation during scheme catalog update
Investor Communication Transaction confirmations within 1 business day Automated email/SMS on every transaction
Data Privacy Protection of investor financial data AES-256 encryption, role-based access control
Fraud Monitoring Suspicious transaction monitoring Real-time anomaly detection, velocity checks

C# Audit Trail Implementation

C#
public class AuditTrailService
{
    private readonly IAuditRepository _auditRepo;
    private readonly IHashService _hashService;

    public async Task RecordEventAsync(
        AuditEvent auditEvent, CancellationToken ct)
    {
        var previousHash = await _auditRepo
            .GetLatestHashAsync(ct);

        var entry = new AuditEntry
        {
            EventId = Guid.NewGuid(),
            EventType = auditEvent.EventType,
            UserId = auditEvent.UserId,
            EntityType = auditEvent.EntityType,
            EntityId = auditEvent.EntityId,
            Action = auditEvent.Action,
            OldValue = auditEvent.OldValue,
            NewValue = auditEvent.NewValue,
            Metadata = JsonSerializer.Serialize(auditEvent.Metadata),
            IpAddress = auditEvent.IpAddress,
            UserAgent = auditEvent.UserAgent,
            Timestamp = DateTime.UtcNow,
            PreviousHash = previousHash
        };

        entry.EntryHash = _hashService.ComputeHash(
            $"{entry.EventId}|{entry.Timestamp}|" +
            $"{entry.UserId}|{entry.EntityType}|" +
            $"{entry.EntityId}|{entry.Action}|" +
            $"{previousHash}");

        await _auditRepo.AppendAsync(entry, ct);
    }
}

public class AuditEntry
{
    public Guid EventId { get; set; }
    public string EventType { get; set; }
    public string UserId { get; set; }
    public string EntityType { get; set; }
    public string EntityId { get; set; }
    public string Action { get; set; }
    public string OldValue { get; set; }
    public string NewValue { get; set; }
    public string Metadata { get; set; }
    public string IpAddress { get; set; }
    public string UserAgent { get; set; }
    public DateTime Timestamp { get; set; }
    public string PreviousHash { get; set; }
    public string EntryHash { get; set; }
}

24. Cost Estimation

Cost estimation for an investment platform must account for the premium infrastructure required for financial-grade reliability, security, and compliance. The costs are higher than typical web applications due to mandatory encryption requirements, redundant database deployments, regulatory audit infrastructure, and the need for multiple integration endpoints with exchanges and regulatory bodies.

Monthly Infrastructure Cost Breakdown

Component Specification Monthly Cost (USD)
Application Servers (8x) c5.2xlarge (8 vCPU, 16 GB RAM) $3,200
PostgreSQL (Primary + 2 Replicas) r6g.2xlarge, 500 GB GP3 $2,400
Redis Cluster (3 nodes) r6g.large, 32 GB total $900
Elasticsearch (3 nodes) r6g.xlarge.elasticsearch, 200 GB $1,200
Kafka (3 brokers) kafka.m5.2xlarge, 500 GB $1,800
S3 Storage 1 TB standard + lifecycle policies $25
CDN (CloudFront) 1 TB/month transfer $85
WAF + Shield Standard DDoS protection, rate limiting $200
Monitoring (Datadog/Grafana) APM, logs, metrics $500
SSL Certificates (Wildcard) Multi-domain coverage $50
Backup & DR Cross-region, 30-day retention $400
Exchange API Fees BSE/MFU connectivity $500
Total Estimated $11,260
Cost Optimization: Use reserved instances for 1-year commitment to save 30-40% on compute. Implement auto-scaling to reduce instance counts during non-market hours. Use S3 Intelligent-Tiering for infrequently accessed audit logs. Consider a managed Kafka service (Confluent Cloud) to reduce operational overhead.

25. Testing Strategy

Testing a financial platform demands significantly more rigor than typical web applications. Incorrect calculations, lost transactions, or data inconsistencies can result in financial losses for investors and regulatory penalties for the platform. The testing strategy must cover unit tests for financial calculation accuracy (XIRR, capital gains, exit load), integration tests for exchange API connectivity, end-to-end tests for complete investment workflows, reconciliation tests that verify data consistency across services, and chaos engineering tests that validate system resilience.

Testing Pyramid for Financial Systems

Layer Count Focus Tools
Unit Tests 5,000+ Financial calculations, lot matching, tax computation xUnit, FluentAssertions
Integration Tests 800+ API contracts, DB operations, Redis caching Testcontainers, WireMock
Contract Tests 200+ BSE/MFU API compatibility, Kafka event schemas Pact, Schema Registry
E2E Tests 150+ Complete workflows: register → KYC → invest → redeem Playwright, Selenium
Reconciliation Tests 50+ Data consistency, balance verification, audit integrity Custom harness
Chaos Tests 30+ Network partition, DB failover, exchange downtime Chaos Monkey, Litmus

C# Financial Calculation Tests

C#
public class XirrCalculatorTests
{
    [Fact]
    public void XIRR_SingleInvestment_ExactReturn()
    {
        var calculator = new XirrCalculator();
        var cashFlows = new List<CashFlow>
        {
            new(new DateTime(2024, 1, 1), -100000m),
            new(new DateTime(2025, 1, 1), 112000m)
        };

        var xirr = calculator.CalculateXIRR(cashFlows);

        Assert.InRange(xirr, 0.119, 0.121);
    }

    [Fact]
    public void XIRR_SIPSequence_ReturnsCorrect()
    {
        var calculator = new XirrCalculator();
        var cashFlows = new List<CashFlow>
        {
            new(new DateTime(2023, 4, 1), -5000m),
            new(new DateTime(2023, 5, 1), -5000m),
            new(new DateTime(2023, 6, 1), -5000m),
            new(new DateTime(2023, 7, 1), -5000m),
            new(new DateTime(2023, 8, 1), -5000m),
            new(new DateTime(2023, 9, 1), -5000m),
            new(new DateTime(2023, 10, 1), -5000m),
            new(new DateTime(2023, 11, 1), -5000m),
            new(new DateTime(2023, 12, 1), -5000m),
            new(new DateTime(2024, 1, 1), -5000m),
            new(new DateTime(2024, 2, 1), -5000m),
            new(new DateTime(2024, 3, 1), -5000m),
            new(new DateTime(2025, 3, 31), 68000m)
        };

        var xirr = calculator.CalculateXIRR(cashFlows);

        Assert.InRange(xirr, 0.10, 0.20);
    }

    [Fact]
    public void ExitLoad_WithinExitWindow_CalculatedCorrectly()
    {
        var calc = new ExitLoadCalculator();
        var lots = new List<PurchaseLot>
        {
            new() { PurchaseDate = DateOnly.FromDateTime(DateTime.Today.AddDays(-30)),
                     Units = 100, PurchaseNAV = 50m, CurrentNAV = 52m }
        };
        var scheme = new Scheme { ExitLoadStructure = "1% within 365 days" };

        var result = calc.CalculateRedemption(lots, 100, scheme);

        Assert.Equal(0.52m, result.ExitLoad, 2);
    }
}

26. Interview Q&A

Q1: How would you handle NAV data arriving late from an AMC?

Answer: The NAV pipeline implements a multi-source fallback strategy. If the primary AMFI feed hasn't arrived by 11:30 PM, we automatically switch to the CAMS RTA feed. If that's also delayed, we check the KFintech feed, and finally fall back to BSE's NAV feed. We also maintain historical NAV prediction models that can estimate NAV within a 0.5% margin for emergency portfolio display, but never for transaction execution. All fallback attempts are logged, and if NAV isn't received by midnight, an escalation alert goes to the ops team. For existing holdings, the last known NAV continues to be used for display with a "NAV pending" indicator.

Q2: How do you ensure portfolio balance consistency across services?

Answer: We use the outbox pattern with exactly-once delivery semantics. When a transaction is recorded, both the transaction record and an outbox event are written in the same database transaction. A Debezium CDC connector publishes outbox events to Kafka. The portfolio engine consumes these events and updates holdings in an idempotent manner using the transaction ID as the deduplication key. We run hourly reconciliation jobs that compare the sum of all transactions against the current holdings for each user. Any discrepancy triggers an immediate alert and puts the affected account into read-only mode until resolved.

Q3: How would you design the SIP scheduler to handle 80 million active SIPs?

Answer: The SIP scheduler uses a date-partitioned approach. SIPs are distributed across processing shards based on their SIP date (1st through 28th). Each shard has a dedicated processing pool. On any given day, we only process the SIPs for that date — roughly 80M / 28 ≈ 2.86M SIPs. These are further distributed across Kafka consumer instances. Each consumer processes a batch of SIPs, generates installment records, and initiates NACH debits. We use batch processing with 100-SIP batches to balance throughput with error isolation. Failed SIPs are moved to a retry queue with exponential backoff (1 hour, 4 hours, next business day).

Q4: Explain how you would implement XIRR calculation for a portfolio with hundreds of transactions.

Answer: XIRR uses Newton-Raphson iteration to find the rate where NPV equals zero. For a portfolio with hundreds of transactions, we collect all cash flows: negative for purchases (investments out), positive for redemptions (money in), and the current portfolio value as the final positive cash flow. We sort by date, calculate years from the first transaction, then iterate. The initial guess matters for convergence — I use the simple return as the starting point. For production, we set a convergence tolerance of 1e-7 and max iterations of 100. If it doesn't converge, we fall back to bisection method. Edge cases include all-zero cash flows (error), single transaction (use CAGR), and mixed signs with no redemption (current value is the terminal cash flow).

Q5: How would you handle a scenario where the exchange (BSE) goes down during market hours?

Answer: We implement circuit breaker pattern with three states: closed (normal), open (exchange down), and half-open (testing recovery). When BSE API fails exceed the threshold (5 consecutive failures or 50% failure rate in 1 minute), the circuit opens. During this state, orders are queued in a durable Kafka topic with guaranteed persistence. We display a "orders queued for processing" message to users. When the circuit transitions to half-open, we process the oldest queued orders first. If BSE recovers, we drain the queue. If it remains down, orders are held until market close. After market hours, any unexecuted orders are automatically cancelled and users are notified. We also have MFU as a fallback routing option for critical orders.

Q6: How do you compute capital gains tax with FIFO lot matching?

Answer: FIFO lot matching maintains a queue of purchase lots for each scheme per user. When units are redeemed, we dequeue from the front (oldest lots first) and match against the redemption quantity. Each matched portion generates a capital gain record with the holding period calculated from the lot's purchase date to the redemption date. Short-term (≤12 months for equity) gains are taxed at 20%, long-term gains above ₹1.25 lakh exemption at 12.5%. The lot tracker must handle edge cases: partial lot consumption (a lot partially redeemed), switches (redeem from one scheme, invest in another), and SIP purchases creating many small lots. We cache the lot state in Redis and persist to PostgreSQL after each transaction for durability.

Q7: How would you design the notification system for financial events?

Answer: Financial notifications must be reliable, timely, and multi-channel. We use a dedicated notification service that consumes events from Kafka topics categorized by priority: critical (order confirmation, payment failure, KYC rejection), important (NAV update, SIP execution, reconciliation failure), and informational (portfolio summary, market update). Critical notifications use synchronous sending with retry and dead letter queue. Important notifications are processed within 5 minutes. Informational notifications are batched and sent during non-peak hours. Each notification is delivered via email (SES), SMS (for critical), push notification, and in-app. We track delivery status and retry failed notifications. All financial communications are archived for regulatory compliance.

Q8: How do you ensure data consistency during a switch transaction between two schemes?

Answer: A switch is atomic: units are redeemed from scheme A and invested in scheme B in a single transaction. We implement this as a distributed saga with compensation. Step 1: Place redemption order for scheme A. Step 2: On confirmation, place purchase order for scheme B. Step 3: On purchase confirmation, update portfolio holdings. If step 2 fails, we compensate by cancelling step 1 (if still possible) or placing a fresh purchase in scheme A. The saga state machine tracks each step, and a background reconciliation job ensures consistency. Both legs use the same NAV date, and the switch is processed through BSE's switch facility which handles the atomicity at the exchange level.

Q9: What are the challenges in building a real-time portfolio dashboard?

Answer: The main challenges are: (1) NAV freshness — mutual fund NAVs update only once daily, so "real-time" means showing the latest NAV with intraday change estimate for equity-heavy funds; (2) Calculation performance — computing XIRR for a user with 50 SIPs and 200 transactions must complete within 200ms, requiring pre-computed snapshots and incremental updates; (3) Concurrency — the same user might have concurrent SIP deductions and manual investments; (4) Caching strategy — cache invalidation on NAV update must be atomic across all users holding that scheme; (5) WebSocket management — for live portfolio value updates, we maintain persistent connections with heartbeat, and use server-sent events as a fallback. We pre-compute portfolio snapshots daily and serve them as the baseline, updating incrementally for same-day transactions.

Q10: How would you handle regulatory circular changes mid-year that affect existing SIPs?

Answer: SEBI periodically issues circulars that change rules (e.g., changing minimum SIP amounts, modifying exit load structures, or introducing new categories). We implement a regulatory change management workflow. First, the compliance team enters the circular details into the admin panel with an effective date. The system then identifies all affected SIPs and schemes. For SIP amount changes, we notify affected investors with a modification request. For scheme restructuring (like category changes), we update the scheme master and trigger a reclassification. All changes are versioned — each SIP record maintains its state at every point in time, and we can audit what rule applied at what date. We also maintain a regulatory calendar in the system to proactively prepare for known upcoming changes.

27. Conclusion

Building an investment and mutual fund platform is one of the most complex and rewarding engineering challenges in fintech. The system demands expertise across distributed systems, financial domain modeling, regulatory compliance, real-time data processing, and security engineering. Every component, from the NAV ingestion pipeline to the SIP scheduler to the portfolio analytics engine, must operate with zero tolerance for data loss or calculation errors. The regulatory environment adds layers of complexity that go beyond typical software engineering concerns, requiring immutable audit trails, KYC compliance, and strict data governance.

The architecture we have designed — microservices with clear domain boundaries, event-driven communication via Kafka, strong consistency for financial data with PostgreSQL, Redis for caching, and Elasticsearch for search — provides the foundation for a scalable, reliable platform. The C# code examples demonstrate production-grade patterns like saga orchestration for complex workflows, outbox pattern for reliable event publishing, circuit breaker for external system resilience, and FIFO lot matching for tax computation.

For system design interviews, the investment platform question tests your ability to reason about financial domain constraints, design systems where correctness is more important than availability, handle complex multi-party workflows (investor, platform, exchange, RTA, AMC, bank), and build systems with comprehensive audit capabilities. The key insight is that financial platforms are not just CRUD applications — they are state machines where every transition must be validated, recorded, and reconciled. Master the financial domain concepts alongside the technical architecture, and you will be well-prepared to design or discuss any investment platform system.

Key Takeaways: Financial platforms prioritize consistency over availability. Always use DECIMAL for monetary values. Implement event sourcing for audit trails. Design for reconciliation from day one. Build circuit breakers for every external integration. Pre-compute portfolio snapshots for performance. And never, ever lose a transaction.

© 2026 Ayodhyya. All rights reserved.