Design an Investment & Mutual Fund Platform
Building Groww, Zerodha Coin, and Kuvera at scale: NAV pipelines, SIP engines, KYC onboarding, and regulatory compliance
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.
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 |
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
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
);
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).
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
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
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
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
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 | On-demand, daily | |
| Tax Saving Certificate | ELSS investments for 80C proof | On-demand, year-end | |
| Investment Proof | All investments for employer verification | 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
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 |
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.