hyperswitch's payment core carries the highest activity risk

Analysis of juspay/hyperswitch at commit 705a321 finds 601 critical functions across 23,830 total, with five god-functions flagged as both structurally extreme and actively changing — led by a CC-103 test orchestration handler and a CC-61 payment response tracker modified zero days ago.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk21.23Low
Hottest FunctionbankRedirectRedirection

Antipatterns Detected

complex_branching5deeply_nested5exit_heavy5god_function5long_function5hub_function1

Run this on your own codebase

See if your own repo has a bankRedirectRedirection-style hotspot — run this in any local git repo:

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god-function and why does it matter in hyperswitch?

A god-function is a function that has accumulated so many responsibilities that it becomes the single point of coupling for a broad set of behaviors — many callers depend on it, and it in turn calls many other functions directly. In structural terms, a high fan-out (the count of distinct functions directly called) is the clearest signal: a fan-out above 15 indicates broad coupling, and above 30 it is a strong refactoring signal. In hyperswitch, `bankRedirectRedirection` has a fan-out of 99 and `build_merchant_enabled_pms_context` has a fan-out of 30 — meaning a change to either can produce unexpected ripple effects across nearly every part of the system it touches. God-functions are particularly hard to test in isolation because reproducing any single execution path requires satisfying the setup preconditions for all the other paths as well.

How do I reduce cyclomatic complexity in Rust?

The most effective technique is extract-method refactoring: identify each independent branch cluster — a match arm, a nested if-else chain, or a loop body with its own sub-conditions — and pull it into a named function with a clear return type. In Rust specifically, returning `Result` or `Option` from extracted functions allows you to use the `?` operator to collapse error-propagation branches that inflate CC in the parent. A cyclomatic complexity above 15 warrants decomposition planning; above 30 it warrants immediate action. For `payment_response_update_tracker` (CC 61), the natural first step is extracting the `Err` arm of the `router_data.response` match into a dedicated `handle_connector_error_response` function — that single extraction should reduce the parent CC by roughly 20 to 25 paths and makes the error handling logic independently reviewable and testable.

Is hyperswitch actively maintained?

The data points clearly to active, ongoing development. Two of the five highest-risk functions — `payment_response_update_tracker` and `build_merchant_enabled_pms_context` — were last modified zero days ago and each received 2 commits in the last 30 days. The entire repository has zero functions in the debt quadrant and 2,545 in the fire quadrant, meaning every structurally complex function in the codebase is also being actively changed. Active development and high structural complexity are not mutually exclusive; in hyperswitch's case they are concurrent, which is precisely what makes the fire-quadrant functions a near-term concern rather than a theoretical one.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. This analysis was run against juspay/hyperswitch at commit SHA `705a321` — check out that commit with `git checkout 705a321` before running the tool to get identical results. Then run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without additional configuration.

What does activity-weighted risk mean?

Activity-weighted risk combines a function's structural complexity — derived from cyclomatic complexity, maximum nesting depth, and fan-out — with how frequently it has been changed by recent commits. A function with a cyclomatic complexity of 80 that has not been touched in two years scores considerably lower than one with a cyclomatic complexity of 20 that is being modified every week, because the dormant complex function poses lower near-term regression risk: no one is introducing bugs into it right now. The score prioritizes where refactoring effort reduces the probability of shipping a defect today, not just where the code looks complicated in the abstract. In hyperswitch, `payment_response_update_tracker` scoring 19.57 with zero days since last change is the clearest expression of that prioritization: it is complex enough to be hard to reason about and active enough that the next change is imminent.

At commit 705a321, juspay/hyperswitch — an open-source payment orchestration engine — contains 23,830 analyzed functions, of which 601 are rated critical and 2,545 fall into the “fire” quadrant: high structural complexity and active recent changes simultaneously. Every one of the five top hotspots is a fire-quadrant god-function, so none of these are backlog cleanup items. I would start with payment_response_update_tracker in payment_response.rs, last modified zero days ago with a risk score of 19.57, because any regression introduced there is in flight right now. The combination of live commit activity and extreme structural depth across the top five makes this a week-of-shipping concern, not a future sprint topic.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
bankRedirectRedirectioncypress-tests/cypress/support/redirectionHandler.js21.21031199
payment_response_update_trackercrates/router/src/core/payments/operations/payment_response.rs19.661723
build_merchant_enabled_pms_contextcrates/router/src/core/payment_methods/cards.rs19.550730
execute_connector_processing_stepcrates/hyperswitch_interfaces/src/api_client.rs17.732623
renderStatusDetailscrates/router/src/core/payment_link/payment_link_status/status.js17.433613

Large Repo Analysis

hyperswitch is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

Codemod / Tooling Files in Results

The file cypress-tests/cypress/support/redirectionHandler.js is test infrastructure rather than application code, but it is not vendored — it is authored and maintained by the Hyperswitch team as part of the end-to-end test suite. It scores highly because the test orchestration logic has accumulated the same structural complexity as the application code it exercises. Similarly, crates/router/src/core/payment_link/payment_link_status/status.js is a JavaScript file embedded in the Rust crate tree for payment link rendering — first-party code, not a library. If you want to exclude the Cypress test support directory from future snapshots to focus analysis on the Rust application core, add { "exclude": ["cypress-tests/cypress/support/"] } to your .hotspotsrc.json. To also exclude the embedded JS status file, extend the pattern: { "exclude": ["cypress-tests/cypress/support/", "crates/router/src/core/payment_link/payment_link_status/*.js"] }.

Quadrant and Pattern Overview

Triage Band Distribution
Fire2545Watch21285

23,830 functions analyzed

Every function in this repository lands in either “fire” or “watch” — there is no dormant structural debt in the quadrant breakdown, which means the complexity that exists is being actively worked on. That is a double-edged finding: the codebase is alive, but the riskiest code is also the code changing most frequently.

Detected Antipatterns
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×5Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.

All five top hotspots share the same antipattern fingerprint: complex branching, deep nesting, exit-heavy control flow, god-function coupling, and long-function length. That uniformity is itself a signal — the same structural pressures appear across the test layer, the payment core, the payment methods pipeline, the connector interface, and the UI status layer. This is not one team’s problem; it is a cross-cutting architectural pattern.


Top 5 Hotspots

bankRedirectRedirection — redirectionHandler.js

bankRedirectRedirection
cypress-tests/cypress/support/redirectionHandler.js
21.23
fire
CC 103
ND 11
FO 99
touches/30d 1
Cyclomatic Complexity 103
threshold: 10
Max Nesting Depth 11
threshold: 4
Fan-Out 99
threshold: 15

This function tops the risk ranking with a risk score of 21.23. Its job, based on the name and source excerpt, is to route Cypress end-to-end redirect flows across a large and growing set of payment connectors — Mifinity, Inespay, Stripe wallet types (AliPay, AmazonPay, Cashapp, RevolutPay, WeChatPay), Adyen wallet types (Dana, GoPay, Momo, Vipps), and more. It does this through a cascading chain of if blocks keyed on connectorId and paymentMethodType, producing a nesting depth of 11 and a cyclomatic complexity of 103. CC 103 means 103 independent execution paths, each of which is a required test case for the function itself — a function that is, ironically, the test infrastructure.

The fan-out of 99 is the most striking structural number here. A single function directly calling 99 distinct helpers and Cypress commands means a change anywhere in that call graph is a potential regression in bankRedirectRedirection, and the reverse is equally true: adding a new connector can unexpectedly disturb logic branches for existing ones. The exit-heavy pattern compounds this — many early return paths, each representing a branch the test suite would need to exercise independently.

The file has 1 touch in the last 30 days and was last changed 4 days ago, with a single author in the last 90 days. Historical signals show no bug-linked commits or reverts, which suggests the function has not demonstrably caused test failures — but with only one committer and CC 103, bus-factor risk and review coverage are real concerns.

Recommendation: Decompose by connector family. Each top-level if (connectorId === "...") block is a candidate for extraction into its own named handler — handleMifinityRedirect, handleStripeWalletRedirect, handleAdyenWalletRedirect, etc. — each with focused test coverage. The coordinator function should do nothing but dispatch. This would bring the CC of the parent below 20 and make each connector path independently testable.


payment_response_update_tracker — payment_response.rs

payment_response_update_tracker
crates/router/src/core/payments/operations/payment_response.rs
19.57
fire
CC 61
ND 7
FO 23
touches/30d 2
Cyclomatic Complexity 61
threshold: 10
Max Nesting Depth 7
threshold: 4

This is the function I consider most urgent from a production risk standpoint. It was last touched zero days ago, has 2 commits in the last 30 days, and carries a risk score of 19.57. Two distinct authors have been active in the file over the last 90 days — concurrent ownership of a CC-61 function is a coordination risk.

From the source excerpt, payment_response_update_tracker handles post-connector-call state reconciliation for a payment attempt: it reads the connector response, processes additional payment method data (handling network tokens, card details for network transaction IDs, and decrypted wallet tokens as distinct cases), optionally encrypts sensitive payment method fields, then branches on whether the router data response is an error or a success, further branching on whether multiple captures are in play. The #[allow(clippy::too_many_arguments)] suppression is itself a signal — Clippy agrees the function signature is overloaded, and the complexity metrics confirm the body matches.

The nesting depth of 7 means there are points where the reader must track seven layers of conditional context simultaneously. In Rust, where each layer may also carry lifetime and ownership implications — particularly around key_manager_state, key_store, and the async await points — the reasoning burden is higher than the raw ND value suggests. The fan-out of 23 means this function reaches broadly across the codebase: the storage layer, domain models, encryption helpers, and payment attempt update constructors all share it as a coupling point.

The #[cfg(feature = "v1")] gate is worth noting: this is the v1 payment flow path. If a v2 path is being developed in parallel, any logic change here must be assessed for whether it needs mirroring or diverging in the v2 equivalent — a cross-feature coordination burden the current structure makes easy to miss.

Recommendation: The match on router_data.response (Ok vs Err branch) and the match on payment_data.multiple_capture_data are natural seams for extraction. Pull the error path into a dedicated handle_connector_error_response function and the success path into handle_connector_success_response. This splits the CC roughly in half at the top level and makes the capture-update logic independently reviewable. Given this function was changed today, the refactoring conversation should happen before the next PR on this file lands.


build_merchant_enabled_pms_context — cards.rs

build_merchant_enabled_pms_context
crates/router/src/core/payment_methods/cards.rs
19.5
fire
CC 50
ND 7
FO 30
touches/30d 2
Cyclomatic Complexity 50
threshold: 10
Fan-Out 30
threshold: 15

With a risk score of 19.5, 2 touches in the last 30 days, and last modified zero days ago, this function sits in the same live-development window as payment_response_update_tracker. The doc comment in the source excerpt is unusually explicit: it states that both the legacy list_payment_methods and the new list_payment_methods_client endpoints call this function. That single sentence documents the blast-radius risk directly — any change here affects two API surfaces simultaneously.

The function orchestrates a two-gate payment method filter pipeline: first a constraint-graph filter via Euclid (cgraph), then session flow routing. It loads all merchant connector accounts, filters them, iterates through enabled payment methods, and assembles a consolidated set of hashmaps. A fan-out of 30 means it reaches into the database layer, the Euclid graph cache, address structures, customer models, and configuration — a broad coupling footprint that makes isolated testing difficult. The #[allow(clippy::too_many_arguments)] suppression appears here too, with nine input parameters, several of which are Option types that generate their own internal branching.

The god-function pattern is confirmed structurally: this function is the shared entry point for payment method listing logic. Any merchant-facing behavior change in PM listing — filtering rules, session routing, eligibility logic — runs through here. With two authors active in the last 90 days and two recent commits, the coordination surface is real.

Recommendation: The two-gate structure the doc comment describes is already a logical decomposition. Extract gate one (Euclid cgraph filtering) and gate two (session flow routing) into separate, independently testable functions that return intermediate result types. build_merchant_enabled_pms_context then becomes a thin coordinator that sequences them. This would reduce the CC significantly and allow each gate to be tested against mock inputs without constructing the full context.


execute_connector_processing_step — api_client.rs

execute_connector_processing_step
crates/hyperswitch_interfaces/src/api_client.rs
17.71
fire
CC 32
ND 6
FO 23
touches/30d 1
Cyclomatic Complexity 32
threshold: 10

This function sits in crates/hyperswitch_interfaces — the interface crate that defines the abstraction boundary between the router core and concrete connector implementations. Its generic signature (T, ResourceCommonData, Req, Resp) combined with multiple lifetime parameters ('a, 'b) and a trait-object receiver (&dyn ApiClientWrapper) means the Rust type system is doing heavy lifting here. CC 32 and ND 6 reflect the branching across CallConnectorAction variants: HandleResponse, UCSConsumeResponse, Avoid, StatusUpdate, HandleResponseWithoutBuildRequest, and at least one more implied by the match structure. Each arm represents a distinct execution path through the connector processing step.

The source excerpt reveals one arm that immediately returns an error with an explicit message: UCSConsumeResponse is marked as invalid in the direct gateway flow, which is a documented architectural constraint enforced at runtime. That kind of cross-system guard embedded inside a CC-32 match is a maintenance hazard — if the UCS gateway system evolves, this function must be revisited.

A fan-out of 23 means this function calls into response handling, error construction, tracing infrastructure, and connector integration trait methods. Because this is in the interfaces crate, changes here potentially affect every connector implementation in the repo. The function was touched once in the last 30 days and was last changed 4 days ago — actively changing, not dormant.

Recommendation: Each CallConnectorAction variant is a self-contained behavior. Decompose the match into per-variant handlers — handle_response_action, handle_status_update_action, etc. — invoked from a thin dispatcher. This brings the CC of the dispatcher close to the number of variants and makes each variant independently testable. Because this is the interface layer, the refactoring also improves the experience for anyone adding a new CallConnectorAction variant in the future.


renderStatusDetails — status.js

renderStatusDetails
crates/router/src/core/payment_link/payment_link_status/status.js
17.37
fire
CC 33
ND 6
FO 13
touches/30d 1
Cyclomatic Complexity 33
threshold: 10

This is the only UI-layer function in the top five, embedded inside the Rust crate tree as a JavaScript file powering the payment link status page. The function receives a paymentDetails object and maps payment status strings — succeeded, failed, cancelled, processing, requires_customer_action, requires_merchant_action, requires_capture, partially_captured, expired — to display assets, translated status labels, and error detail items via a large switch statement. CC 33 with ND 6 for what is essentially a status-to-display mapping reflects how much branching lives inside each case arm, particularly the failed case, which constructs error code and message nodes from unified or fallback fields.

The exit-heavy pattern applies across break statements and implicit fall-through guards. The fan-out of 13 means the function calls into item-creation helpers, translation accessors, and date formatting — enough coupling that changing how an individual status renders requires understanding the full function context. The default arm catches unknown statuses with a generic error image, which is the right defensive behavior, but it also means any new payment status not added to the switch will silently render as an error state.

The function was touched once in the last 30 days and was last changed 4 days ago. With a single author over the last 90 days, coordination risk is low — but behavioral fragility is high. Each new payment status Hyperswitch adds at the API layer requires a corresponding case here, and there is no structural enforcement of that contract.

Recommendation: Replace the monolithic switch with a status-to-config map object: each status string maps to a plain data object containing imageSource, statusKey, messageKey, and an optional item-builder function. renderStatusDetails then looks up the config and applies it. This drops the CC to near 1 for the dispatch logic, moves per-status configuration to a declarative structure that is easy to audit and extend, and makes missing status coverage immediately visible as a gap in the map.


What the Pattern Distribution Tells Me

All five functions share every Tier 1 antipattern: complex branching, deeply nested control flow, exit-heavy return paths, god-function coupling, and long-function length. That is not coincidence — it reflects a common growth pattern where functions accumulate responsibility incrementally as new connectors, payment methods, and status variants are added, each addition being the smallest safe change at the time. The result is functions that are individually reasonable to extend but collectively difficult to reason about, test, or hand off.

The context_only functions offer a useful contrast. to_connector_auth_type in crates/router/tests/connectors/utils.rs has 2 touches in the last 30 days and CC 12 — active but not structurally concerning, a watch-quadrant function that warrants monitoring as connector coverage grows. foreign_from in transformers.rs follows a similar profile: 1 touch in the last 30 days, CC 5, watch quadrant — not a refactoring priority. These are the functions the fire-quadrant hotspots should aspire to resemble after decomposition.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
deeply_nested5
exit_heavy5
god_function5
long_function5
hub_function1

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function.

Reproduce This Analysis

git clone https://github.com/juspay/hyperswitch
cd hyperswitch
git checkout 705a3219504c3cf9034e71d956455f8834ea4e64
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Was this useful? Let me know →

Related Analyses