- Juspay (Bengaluru) builds payments infrastructure: checkout SDKs, payment orchestration, UPI stacks and the open-source Hyperswitch. Much of its codebase uses Haskell, PureScript and Rust.
- Freshers typically enter via the Juspay hiring challenge: an online coding round, a development or problem-solving stage, and interviews. Rounds vary by edition.
- The coding round is famous for graphs where every node has at most one outgoing edge: maximum weight node, nearest meeting cell, largest sum cycle. Solved below.
- Interviews frequently feature the m-ary tree lock/unlock/upgrade problem, often with a thread-safety follow-up.
- Freshers usually join as interns (often around six months) with conversion to full-time based on performance. Approximate public-source FTE offers: roughly 18 to 30 LPA. Confirm current terms with Juspay.
Juspay is one of the few Indian companies where the interview tells you something real about the job. The coding problems are tight graph and tree puzzles that reward precise reasoning, the follow-ups probe concurrency because payment systems are concurrent, and the company openly bets on functional programming, which most Indian graduates have never used.
This guide covers the commonly reported hiring challenge format, gives complete solutions to the problems that appear most often in candidate write-ups (in Python and Java with dry runs), explains the tree locking problem and its concurrency follow-up, introduces the Haskell and PureScript ideas worth knowing, and explains how internships convert to full-time roles. Juspay changes the challenge structure between editions, so always read the official announcement for your cycle.
About Juspay: What the Company Builds
Juspay builds payment infrastructure used by large Indian merchants and banks. Its products include checkout and payment SDKs used inside many popular apps, payment orchestration that routes transactions across gateways, UPI technology stacks for banks, and Hyperswitch, an open-source payments switch written in Rust. When you pay inside a well-known Indian app, there is a reasonable chance some Juspay software is in the path.
Technically, Juspay is known for unusual choices. Large parts of its backend have been written in Haskell, its client-side and SDK work has used PureScript (a Haskell-like language that compiles to JavaScript), and newer infrastructure such as Hyperswitch uses Rust. The common thread is strong static typing: in payments, catching a class of bugs at compile time is worth a lot.
The Juspay Hiring Challenge: Commonly Reported Structure
Online coding
A few algorithmic problems on an online platform, frequently graph-based, with hidden test cases. Usually 60 to 120 minutes.
Development / problem round
Some editions include a longer build or problem-solving stage that evaluates code quality and design, not only correctness.
Technical interviews
One or two interviews: DSA (often the tree locking problem), your code from earlier rounds, CS fundamentals, projects.
Culture / leadership
Motivation, learning attitude, openness to functional programming, and how you handle ambiguity.
Solved: Juspay's Signature Graph Problems
The classic problem set uses a functional graph: N cells numbered 0 to N−1, and an array edge where edge[i] is the single cell that cell i exits to, or −1 if it has no exit. Because every node has out-degree at most one, each connected component is either a chain ending at −1, or a single cycle with trees hanging into it. That structure makes O(N) solutions possible, which is the point of the problems.
Problem 1: Maximum weight node
The weight of a cell is the sum of the indices of all cells that point to it. Return the cell with the maximum weight; if several tie, return the largest index.
def max_weight_node(edge):
n = len(edge)
weight = [0] * n
for i, e in enumerate(edge):
if e != -1:
weight[e] += i
best = 0
for i in range(n):
if weight[i] >= weight[best]: # >= so ties pick the larger index
best = i
return best
print(max_weight_node([2, 0, -1, 2])) # cell 2 gets 0 + 3 = 3 -> 2
O(N) time and space. The only trap is the tie rule, so read the statement carefully: some versions say largest index, others smallest.
Problem 2: Nearest meeting cell
Given two cells C1 and C2, find a cell reachable from both that minimises the total distance dist(C1, x) + dist(C2, x). Return −1 if none exists. (Some versions minimise the maximum of the two distances instead, as in LeetCode 2359; the method is identical, only the final comparison changes.)
Because each node has one outgoing edge, the path from a start node is a simple walk: follow edges until you hit −1 or revisit a node. Record distances from C1, then from C2, then compare.
def distances(edge, start):
dist = [-1] * len(edge)
d, cur = 0, start
while cur != -1 and dist[cur] == -1:
dist[cur] = d
d += 1
cur = edge[cur]
return dist
def nearest_meeting_cell(edge, c1, c2):
d1, d2 = distances(edge, c1), distances(edge, c2)
best, best_cost = -1, float("inf")
for i in range(len(edge)):
if d1[i] != -1 and d2[i] != -1:
cost = d1[i] + d2[i]
if cost < best_cost:
best, best_cost = i, cost
return best
edge = [4, 4, 1, 4, 13, 8, 8, 8, 0, 8, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, 22, 21]
print(nearest_meeting_cell(edge, 9, 2)) # 4
import java.util.Arrays;
public class NearestMeetingCell {
static int[] distances(int[] edge, int start) {
int[] dist = new int[edge.length];
Arrays.fill(dist, -1);
int d = 0, cur = start;
while (cur != -1 && dist[cur] == -1) {
dist[cur] = d++;
cur = edge[cur];
}
return dist;
}
static int nearest(int[] edge, int c1, int c2) {
int[] d1 = distances(edge, c1), d2 = distances(edge, c2);
int best = -1;
long bestCost = Long.MAX_VALUE;
for (int i = 0; i < edge.length; i++) {
if (d1[i] != -1 && d2[i] != -1 && (long) d1[i] + d2[i] < bestCost) {
bestCost = (long) d1[i] + d2[i];
best = i;
}
}
return best;
}
public static void main(String[] args) {
int[] edge = {4, 4, 1, 4, 13, 8, 8, 8, 0, 8, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, 22, 21};
System.out.println(nearest(edge, 9, 2)); // 4
}
}
Dry run for C1 = 9, C2 = 2 on the sample: from 9 the walk is 9 → 8 → 0 → 4 → 13 → 11 → 9 (revisit, stop), giving distances 9:0, 8:1, 0:2, 4:3, 13:4, 11:5. From 2 the walk is 2 → 1 → 4 → 13 → 11 → 9 → 8 → 0 → 4 (revisit), giving 2:0, 1:1, 4:2, 13:3, 11:4, 9:5, 8:6, 0:7. Common cells and total costs: 4 → 3+2 = 5, 13 → 4+3 = 7, 11 → 5+4 = 9, 9 → 0+5 = 5, 8 → 1+6 = 7, 0 → 2+7 = 9. Cells 4 and 9 tie at 5; scanning in index order keeps the first, cell 4. If the problem specifies a different tie rule, adjust the comparison. Each walk is O(N), so the whole solution is O(N).
Problem 3: Largest sum cycle
Return the maximum, over all cycles, of the sum of node indices in the cycle, or −1 if there is no cycle. Use the three-state colouring trick for functional graphs: walk from every unvisited node, marking nodes with the id of the current walk. If you reach a node marked with the current walk id, you found a new cycle; sum it. If you reach a node from an earlier walk or −1, stop. Every node is visited once, so it is O(N).
def largest_sum_cycle(edge):
n = len(edge)
walk_id = [-1] * n # which walk first visited each node
best = -1
for start in range(n):
if walk_id[start] != -1:
continue
cur = start
while cur != -1 and walk_id[cur] == -1:
walk_id[cur] = start
cur = edge[cur]
if cur != -1 and walk_id[cur] == start: # cycle found in this walk
total, node = cur, edge[cur]
while node != cur:
total += node
node = edge[node]
best = max(best, total)
return best
print(largest_sum_cycle([4, 4, 1, 4, 13, 8, 8, 8, 0, 8, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, 22, 21])) # 45
static long largestSumCycle(int[] edge) {
int n = edge.length;
int[] walkId = new int[n];
java.util.Arrays.fill(walkId, -1);
long best = -1;
for (int start = 0; start < n; start++) {
if (walkId[start] != -1) continue;
int cur = start;
while (cur != -1 && walkId[cur] == -1) {
walkId[cur] = start;
cur = edge[cur];
}
if (cur != -1 && walkId[cur] == start) {
long total = cur;
for (int node = edge[cur]; node != cur; node = edge[node]) total += node;
best = Math.max(best, total);
}
}
return best;
}
On the sample, the cycle 4 → 13 → 11 → 9 → 8 → 0 → 4 sums to 4 + 13 + 11 + 9 + 8 + 0 = 45, and 21 → 22 → 21 sums to 43, so the answer is 45. Use long for the sum in Java: with N up to 10^5 or more, index sums can exceed the int range. That is exactly the kind of hidden test case that separates passing from failing. Our graph BFS and DFS guide covers cycle detection more generally.
The Juspay Tree Locking Problem (Lock, Unlock, Upgrade)
This problem is widely reported in Juspay interviews. You have an m-ary tree of N nodes. Implement three operations, each taking a node and a user id:
- lock(X, uid): succeeds if X is not locked, no ancestor of X is locked, and no descendant of X is locked. Then X is locked by uid.
- unlock(X, uid): succeeds only if X is locked by the same uid.
- upgradeLock(X, uid): succeeds if X is not locked, no ancestor is locked, X has at least one locked descendant, and all locked descendants are locked by uid. Then all those descendants are unlocked and X is locked by uid.
The naive solution scans the whole subtree on every lock, which is O(N) per call. The efficient idea: store in each node a count of locked descendants. Then "no descendant is locked" is an O(1) check, and lock/unlock only walk the ancestor chain, O(h) where h is the tree height (O(log_m N) for a complete tree).
import java.util.*;
public class TreeOfSpace {
static class Node {
final String name;
Node parent;
final List<Node> children = new ArrayList<>();
int lockedBy = -1;
int lockedDescendants = 0;
Node(String name) { this.name = name; }
}
private boolean ancestorLocked(Node x) {
for (Node p = x.parent; p != null; p = p.parent)
if (p.lockedBy != -1) return true;
return false;
}
private void updateAncestors(Node x, int delta) {
for (Node p = x.parent; p != null; p = p.parent) p.lockedDescendants += delta;
}
public boolean lock(Node x, int uid) {
if (x.lockedBy != -1 || x.lockedDescendants > 0 || ancestorLocked(x)) return false;
x.lockedBy = uid;
updateAncestors(x, +1);
return true;
}
public boolean unlock(Node x, int uid) {
if (x.lockedBy != uid) return false;
x.lockedBy = -1;
updateAncestors(x, -1);
return true;
}
public boolean upgradeLock(Node x, int uid) {
if (x.lockedBy != -1 || x.lockedDescendants == 0 || ancestorLocked(x)) return false;
List<Node> locked = new ArrayList<>();
collectLocked(x, locked);
for (Node d : locked) if (d.lockedBy != uid) return false;
for (Node d : locked) unlock(d, uid);
return lock(x, uid);
}
// Visit only subtrees that contain locked nodes
private void collectLocked(Node x, List<Node> out) {
for (Node c : x.children) {
if (c.lockedBy != -1) out.add(c);
if (c.lockedDescendants > 0) collectLocked(c, out);
}
}
public static void main(String[] args) {
// Build a 2-ary tree in level order: World -> Asia, Africa -> India, China, Egypt, SouthAfrica
String[] names = {"World", "Asia", "Africa", "India", "China", "Egypt", "SouthAfrica"};
int m = 2;
Node[] nodes = new Node[names.length];
for (int i = 0; i < names.length; i++) nodes[i] = new Node(names[i]);
for (int i = 0; i < names.length; i++)
for (int k = 1; k <= m && m * i + k < names.length; k++) {
nodes[i].children.add(nodes[m * i + k]);
nodes[m * i + k].parent = nodes[i];
}
TreeOfSpace t = new TreeOfSpace();
System.out.println(t.lock(nodes[3], 9)); // India by 9: true
System.out.println(t.lock(nodes[4], 9)); // China by 9: true
System.out.println(t.lock(nodes[1], 9)); // Asia: false (descendants locked)
System.out.println(t.upgradeLock(nodes[1], 9)); // Asia upgrade by 9: true
System.out.println(t.unlock(nodes[3], 9)); // India already released: false
System.out.println(t.lock(nodes[0], 3)); // World: false (descendant Asia locked)
}
}
Complexity and the follow-up interviewers love
lock and unlock are O(h). upgradeLock is O(h + k·h + size of explored subtrees) where k is the number of locked descendants, since collection prunes subtrees with no locks. Then comes the follow-up: make it thread-safe for concurrent calls without a single global lock. Good discussion points:
- A global
synchronizedis correct but serialises everything; say so first as the baseline. - Per-node locks acquired in a consistent order (root to target) avoid deadlock, but locking the whole ancestor chain still contends heavily at the root.
- An optimistic approach: atomically claim the target node with compare-and-set on
lockedBy, then validate ancestors and increment counters atomically; on any conflict, roll back the changes made so far and return false. - Discuss the race you must prevent: two users locking a parent and a child at the same time, each seeing the other as unlocked.
You do not need a perfect lock-free design. Interviewers mostly want to see that you can identify the races, propose a correct scheme, and reason about its cost. The Java concurrency guide covers the primitives you would use.
Haskell and PureScript: What You Should Know Before Joining
You will not be asked to write production Haskell in the interview, but knowing the core ideas helps you answer "why do you want to work here" honestly and shows you can learn fast. Here are the concepts that matter, illustrated with a payments flavour.
Pure functions, immutability and Either for errors
data PaymentError = InvalidAmount | UnsupportedCurrency String
deriving Show
data Payment = Payment { amount :: Integer, currency :: String }
deriving Show
validateAmount :: Integer -> Either PaymentError Integer
validateAmount a
| a <= 0 = Left InvalidAmount
| otherwise = Right a
validateCurrency :: String -> Either PaymentError String
validateCurrency c
| c `elem` ["INR", "USD"] = Right c
| otherwise = Left (UnsupportedCurrency c)
-- Applicative style: the first error short-circuits
mkPayment :: Integer -> String -> Either PaymentError Payment
mkPayment a c = Payment <$> validateAmount a <*> validateCurrency c
-- The same thing with do-notation
mkPayment' :: Integer -> String -> Either PaymentError Payment
mkPayment' a c = do
a' <- validateAmount a
c' <- validateCurrency c
pure (Payment a' c')
totalPaise :: [Payment] -> Integer
totalPaise = foldr (\p acc -> amount p + acc) 0
main :: IO ()
main = do
print (mkPayment 50000 "INR") -- Right (Payment {amount = 50000, currency = "INR"})
print (mkPayment 0 "INR") -- Left InvalidAmount
print (mkPayment 100 "XYZ") -- Left (UnsupportedCurrency "XYZ")
print (totalPaise [Payment 100 "INR", Payment 250 "INR"]) -- 350
Notice what this buys you: there is no way to construct a Payment without passing validation, errors are values in the type signature instead of hidden exceptions, and amounts are integers in paise. The compiler forces every caller to handle the Left case. This is the practical argument for functional programming in payments, and being able to articulate it in your culture round is valuable.
Concepts to be able to explain
| Concept | One-line explanation | Mainstream analogue |
|---|---|---|
| Pure function | Output depends only on input; no side effects | A static method that touches no shared state |
| Immutability | Values never change after creation | Java records, Kotlin val, final fields |
| Maybe / Either | Absence or failure encoded in the type | Optional, Result types in Rust |
| map / filter / fold | Transform collections without loops | Java streams, Python comprehensions, reduce |
| Type classes | Ad-hoc polymorphism via constraints | Interfaces, Rust traits |
| Algebraic data types | Sum and product types with exhaustive pattern matching | Sealed interfaces with switch patterns, Rust enums |
| IO / effects in types | Side effects are visible in signatures | No direct equivalent; closest is explicit dependency injection |
PureScript applies the same ideas to front-end and SDK code, compiling to JavaScript. Rust, used in Hyperswitch, borrows many of these ideas (enums with data, Result, Option, traits) while adding ownership for memory safety. If you have a weekend, work through the first chapters of an introductory Haskell book and write a small CLI in Haskell; mentioning a concrete thing you built is far stronger than saying "I am interested in FP".
Juspay Technical and Culture Interviews
Technical interview themes
- Tree locking or a similar data structure design problem with complexity analysis and concurrency follow-ups.
- Your earlier submissions: why you chose an approach, how you would change it, what edge cases you considered.
- CS fundamentals: OS (threads, locks, deadlocks), DBMS (transactions, indexes), networking basics.
- Payments intuition: idempotency, retries, what happens if a bank callback never arrives. The UPI payment system design post is good background reading.
- Projects: be ready for a deep dive on anything on your resume.
Culture round
Juspay places weight on learning ability and ownership. Expect questions such as: what have you learned on your own recently and how; a time you were stuck and how you got unstuck; why Juspay specifically; how you feel about learning a language like Haskell. Honest curiosity beats polished buzzwords here.
Internship to Full-Time: How Conversion Typically Works
For freshers, Juspay commonly hires through long internships, frequently around six months, that convert into full-time roles based on performance. This is different from short summer internships at most product companies.
- Clear the hiring challenge and receive an internship offer with a stipend and, in some cases, a stated full-time package on conversion.
- Onboard and learn the stack, often including functional programming training in the early weeks.
- Ship real work on a product team. Reviews look at code quality, learning speed and ownership.
- Conversion decision near the end of the internship. Strong interns receive full-time offers.
Juspay Salary in 2026
| Stage | Approximate figures | Notes |
|---|---|---|
| Intern | Monthly stipend, varies by edition | Check your offer letter; stipend is separate from FTE CTC |
| Fresher full-time (on conversion) | ~18 to 30 LPA total | May include variable pay or ESOP components |
| Engineer, 2 to 5 years | ~25 to 45 LPA | Wide spread depending on role and negotiation |
| Senior / lead | ~40 LPA and above | ESOPs can be a meaningful part at senior levels |
Approximate 2026 ranges from public sources such as AmbitionBox, Glassdoor and levels.fyi; varies by team, location and negotiation. If an offer includes ESOPs, understand vesting, strike price and liquidity before valuing them; our ESOP guide for Indian startups explains how.
A 4-Week Juspay Preparation Plan
| Week | Focus | Deliverable |
|---|---|---|
| 1 | Graph fundamentals: BFS, DFS, cycle detection, functional graphs | Solve the three problems above from scratch in under 45 minutes total, plus 10 similar problems |
| 2 | Trees and data structure design | Implement tree locking from memory; LRU cache; serialize/deserialize tree |
| 3 | Concurrency and fundamentals | Write a thread-safe version of tree locking; revise OS and DBMS basics |
| 4 | Functional programming and mocks | Write a small Haskell or PureScript program; two mock interviews; polish project stories |
Juspay's process favours candidates who think carefully rather than fast. Slow down on the coding round, read every tie-break rule, use 64-bit sums, and in interviews narrate the races and edge cases you are guarding against. Do that, show real curiosity about functional programming, and you will stand out in a challenge that many candidates approach as just another online test.
Frequently Asked Questions
Get Strong at Graph and Tree Problems
Prepflix's DSA program builds the graph, tree and concurrency reasoning that Juspay's hiring challenge and interviews test, with mock interviews for practice.
Explore the Prepflix Course →