Backend Development 17 min read

Design and Implementation of a Scalable Fund Routing Decision Engine at Shopee

Shopee’s scalable fund‑routing decision engine combines a configurable Go‑based rule engine with Redis‑backed, Lua‑driven quota control to dynamically match loan orders to external capital providers, enforce multi‑dimensional volume limits, achieve tens of thousands TPS, and provide extensible risk‑management capabilities across markets.

Shopee Tech Team
Shopee Tech Team
Shopee Tech Team
Design and Implementation of a Scalable Fund Routing Decision Engine at Shopee

As Shopee expands in overseas markets, its credit‑related services (e‑commerce installment, cash‑loan, etc.) have grown. To scale, comply with local regulations, and control business risk, Shopee needs to connect external capital providers through joint‑loan and asset‑securitization (ABS) mechanisms.

The fund‑routing platform acts as a bridge between external financial institutions and internal credit products. Because each external capital provider imposes requirements on loan users, loan orders and loan amounts, the platform must match each loan order with a suitable capital provider while respecting volume limits and cost constraints.

The core of the solution is a highly extensible rule decision module and a high‑performance, precise transaction volume‑control module .

Module Design

The fund‑routing decision engine is an independent unit within the fund‑integration system. It receives loan‑related data, applies capital‑provider rules, enforces loan‑volume limits, and outputs the selected capital provider. The overall flow is illustrated below.

The engine is invoked by the transaction service, with clearly defined inputs (loan information) and outputs (selected capital provider).

Capital‑provider order configuration : flexible ordering of providers.

Rule configuration : flexible definition of basic rules, source data, and provider‑specific rules.

Routing time windows : providers can be disabled for specific time periods.

Lazy loading of source data : only load data required by a particular provider.

Threshold management : complex threshold structures can be configured per rule.

Custom validation functions : allow bespoke logic for copied rules.

Rule engine : a component embedded in the application that separates business decisions from code using predefined semantic modules.

Real‑time quota calculation and control : precise per‑day, per‑period, per‑loan‑type accounting to avoid overselling.

Metric reporting : decision results are written to a time‑series database for dashboards and alerts.

Rule Decision Module

Capital providers impose constraints such as prohibited occupations, loan‑amount ranges, minimum age, or minimum credit limit. The platform also requires that the provider’s interest rate be lower than the user‑facing rate. Hard‑coding these checks would be inflexible, so a configurable rule engine is needed.

A rule consists of an operator (e.g., >, <, =, range), a data source (user age, loan amount, etc.), and a threshold (age range, allowed provinces). These are stored in database tables for dynamic use.

Two tables are defined for the rule metadata:

rule_define_tab : stores basic rule definitions (operator code, pre‑processing, description).

rule_item_tab : stores attribute metadata (attribute name, source, type, loading mode).

funder_rule_info_tab : links providers to specific rules, attributes, and thresholds for the rule engine.

Rule Engine Research and Application

Most financial systems use Java‑based engines (Drools, Esper, Activiti, Flowable). Since Shopee’s service is built in Go, the team evaluated Go rule engines and selected govaluate for its simplicity and performance. A typical rule execution looks like this:

func Compare(left interface{}, operator string, right interface{}) (bool, error) {
    var params = make(map[string]interface{})
    params["left"] = left
    params["right"] = right
    var expr *govaluate.EvaluableExpression
    expr, _ = govaluate.NewEvaluableExpression(fmt.Sprintf("left %s right", operator))
    eval, err := expr.Evaluate(params)
    log.Infof("expr=%v,params=%+v,result=%+v", expr.String(), params, eval)
    if err != nil {
        return false, err
    }
    if result, ok := eval.(bool); ok {
        return result, nil
    }
    return false, errors.New("convert error")
}

The function receives the attribute value (left), the threshold (right), and the operator, all configured in the database. It evaluates the expression and returns a boolean result, providing a highly extensible and configurable rule check.

Volume‑Control Module

The system must limit loan exposure on multiple dimensions (total balance, daily count, etc.). Direct updates to a relational database become a bottleneck under high concurrency.

MySQL Pessimistic Lock

Using row‑level locks to update a counter yields ~200 TPS with ~5 ms per update, insufficient for large‑scale financial traffic.

Queue‑Based Asynchronous Processing

Transactions are cached, then persisted via a message queue with multi‑copy and disk‑write guarantees. Although this supports high concurrency, it introduces complexity: cache refresh must recompute thresholds, and a deduplication table is needed to avoid double processing.

Redis Cache + Lua Script

Insert operations are cheap; the bottleneck is balance updates. The solution caches quota information per provider in Redis and performs atomic check‑and‑decrement using a single‑threaded Lua script. Successful checks insert a DB record; a background job later aggregates these records back to the DB.

Cache keys follow the pattern funder_hotspot_limit_khash_{funderId} . Cache refresh calculates the usable quota as threshold - (db_used + pending_db) , then atomically updates the Redis hash.

The control flow (dark nodes = application logic, light nodes = Lua script) ensures no overselling while maintaining high throughput. Tests on a machine with Redis 6.2.2 and MySQL 5.6 show the combined cache‑check‑insert pattern can sustain tens of thousands of TPS under heavy load.

Application and Monitoring

After functional and performance testing, the solution met all requirements: flexible rule configuration, precise loan‑quota control, and high‑throughput processing. To guard against rule mis‑configurations, metrics such as rule‑rejection count and match count are collected and visualized via Prometheus, with alerts for abnormal spikes.

Conclusion and Outlook

The deployed engine routes loan orders across multiple markets, offering flexible rule configuration, strong extensibility, and precise multi‑dimensional control without any overselling. The rule‑decision approach can be applied to other risk‑control scenarios (e.g., SMS/notification gating), while the hot‑spot cache solution is useful for high‑concurrency payment or marketing controls. Future work includes packaging rules as reusable rule‑packs for easier business activation.

rule engineBackend DevelopmentGoperformance testingMySQLRedisfund routing
Shopee Tech Team
Written by

Shopee Tech Team

How to innovate and solve technical challenges in diverse, complex overseas scenarios? The Shopee Tech Team will explore cutting‑edge technology concepts and applications with you.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.