Articles
Nubank Pair Programming Interview: Codebase Changes, Tests, and AI Tool Rules
Prepare for Nubank pair programming: read existing code, fix failing tests, verify state changes, and clarify AI tool rules using dated candidate reports.
For a Nubank pair programming interview, prepare to understand an existing implementation, diagnose a failing test, make a focused change, and explain how you verified it. Confirm the format and AI permissions in your own invitation. Recent candidate accounts describe codebase changes and permitted AI assistance, but they do not establish a single process for every country, level, or team.
A difficult moment comes when deciding whether a failing test reveals a bug, an unclear requirement, or an assumption you introduced while reading the code. This guide gives you a way to handle that moment with or without AI.
What is official, reported, and still invitation-specific?
Official information: Nubank's software-engineering hiring overview, carrying a November 2023 publication date, describes a take-home exercise and pairing with an engineer. The page emphasizes explaining decisions, receiving feedback, and adapting solutions. We checked the current page, but its publication date does not prove that every paragraph has remained unchanged or that every current role follows its sequence.
Candidate reports: In a March 2026 Brazilian community discussion, an IC7 applicant preparing for pairing said there had been no prior take-home and that AI support was permitted. A commenter describing an IC4 experience reported existing code, failing tests, a bug fix, and a feature extension. The applicant's question and the commenter's completed experience are different kinds of evidence.
In a separate June 2026 discussion, a candidate reported completing implementations and using a supplied AI tool for a requested unit test. These Portuguese-language Brazilian reports support a preparation scenario. They do not confirm global AI access, your platform, your language, or an official changeover date.
| Evidence | What it supports | What it cannot settle |
|---|---|---|
| Official overview, dated 2023 | Pairing and collaborative reasoning are part of the described process | Your complete current sequence |
| March 2026 applicant and commenter | Different preparation paths and an existing-code example were reported | A universal take-home or AI rule |
| June 2026 candidate account | AI assistance was reportedly available in that session | Permission to bring any external assistant |
| Your invitation and recruiter clarification | The instructions you should follow | A guarantee about other candidates' interviews |
Our editorial recommendation is to prepare two modes: independent implementation and permitted-tool-assisted review. Both should produce the same explanation of the requirements, the patch, and the tests.
Before pairing, resolve the rules that change preparation
Ask whether you will receive an existing project, whether setup happens in advance, and which language and environment you should use. If there is a take-home, clarify how it connects to pairing. If there is no take-home, do not interpret its absence as evidence that the round will be an isolated algorithm puzzle.
For AI, ask about the specific tool and permitted actions. A platform-provided assistant, browser search, external chat, autocomplete, and an agent that edits files are not interchangeable. Clarify whether prompts or generated changes should be shared with the interviewer and whether any supplied material may leave the interview environment.
A concise message is enough: “Could you confirm the coding environment, whether we start from an existing project, and which AI or documentation tools are permitted during pairing?” You are resolving working conditions, not requesting the hidden task.
If no answer arrives, begin the session by confirming the rules and work independently until permission is clear. Preparing without an assistant remains useful even when one is available: it gives you a baseline for judging whether a suggestion improves the code.
Read the code through a failing behavior
Once you have the project, run the documented test command before changing anything. Record the failing assertion, its expected and actual values, and whether the failure is repeatable. If the suite needs dependencies or services that are unavailable, identify that as setup uncertainty rather than inventing a business-logic fix.
Follow the failing input through the smallest relevant path. Find where data is validated, where state changes, and where the result is produced. A common risk in stateful code is checking a condition after mutation. The function may correctly raise an exception while leaving the object corrupted.
Use a hypothesis that can be disproved. “A rejected operation changes the balance” predicts a specific observation. “The design is bad” does not. Run the operation on a fresh object, inspect both the returned outcome and the state afterward, and compare with the contract.
Do not start by improving every name or adding a framework. First demonstrate that you understand the observed failure. A partner can then challenge your assumption or suggest a missing case before the patch grows.
Original exercise: reject a spend without changing state
This is an original teaching exercise using credits, not a Nubank question or a production financial system. Assume one in-memory object, sequential calls, and positive integer amounts validated at the boundary. A wallet starts with 100 credits. Spending 30 should leave 70; spending more than the available balance should fail without changing anything.
Here is a deliberately incorrect operation:
def spend_bad(wallet, amount):
wallet.balance -= amount
if wallet.balance < 0:
raise ValueError("insufficient credits")
return wallet.balance
Calling it with 101 raises the expected error, but the balance becomes minus one. A test that checks only the exception misses the bug. Before fixing it, write a test that also asserts the balance remains 100.
The minimal repair validates before mutation:
if amount <= 0:
raise ValueError("positive amount required")
if amount > self.balance:
raise ValueError("insufficient credits")
self.balance -= amount
return self.balance
Explain why you chose the comparison. Spending exactly the available balance is allowed, so rejecting amount >= self.balance would introduce another bug. Tests for 99 and 101 are not substitutes for the exact-balance case.
Now your partner changes the requirement: callers may retry a request. Add a request identifier so an identical retry does not spend twice. Before coding, clarify what happens if the same identifier is reused with a different amount and whether a retry returns the original result or the current balance.
For this exercise, reuse with a different amount is a conflict, and a valid retry returns the original result. Failed attempts are not recorded as successful operations. These are explicit teaching choices, not claims about Nubank's implementation.
Add replay behavior without hiding a different bug
Store the amount and result of each successful request. On a retry, compare the payload before returning the saved result:
def spend(self, request_id, amount):
if amount <= 0:
raise ValueError("positive amount required")
if request_id in self.applied:
previous_amount, result = self.applied[request_id]
if amount != previous_amount:
raise ValueError("request conflict")
return result
if amount > self.balance:
raise ValueError("insufficient credits")
self.balance -= amount
self.applied[request_id] = (amount, self.balance)
return self.balance
The object begins with self.balance = 100 and self.applied = {}. Consider three calls: request A spends 30, request B spends 20, then A is retried. The retry returns A's original result, 70, while the current stored balance remains 50. Returning the current balance would violate the particular response contract we chose, even though it would not double-spend.
| Test | Observation that matters |
|---|---|
| Spend exactly 100 from 100 | Returns zero, not an error |
| Attempt to spend 101 from 100 | Balance and request history remain unchanged |
| Repeat A with the same amount | No second state change |
| Repeat A with a different amount | Conflict, with no extra debit |
| Retry A after successful B | Original result is returned; current balance is preserved |
We executed eight local unit tests covering these cases plus normal, zero, and negative amounts. All passed. The result validates a sequential in-memory exercise only. It does not establish durable idempotency, thread safety, distributed transactions, or correctness across a process crash.
If asked to discuss production, name the missing boundary: checking the request identifier and committing the state change must be coordinated durably. Two concurrent workers cannot safely rely on an ordinary dictionary check. Keep that discussion separate from claiming the small exercise already solves it.
If AI is allowed, review its decisions rather than its confidence
The following is an editorial practice routine, conditional on explicit permission. Give the tool the smallest authorized context needed to inspect a failure, and ask it to explain a proposed change before accepting it. Do not paste private recruitment materials into an unapproved service.
For the wallet exercise, a useful review question is: “Which state changes can occur before an error, and which test would expose that?” A less useful request is “Make all tests pass.” The latter can invite a patch that changes expectations instead of preserving the requirement.
Compare each suggestion with a short decision record:
| Suggested patch | Human check | Decision in this exercise |
|---|---|---|
| Reject an amount equal to the balance | Does exact-balance spending belong to the valid domain? | Reject the suggestion; zero remaining balance is valid |
| Deduplicate using only the request identifier | What if the amount changes? | Require payload consistency before replay |
| Return the current balance on replay | What response did we promise? | Return the stored original result |
| Add a lock and call it production-ready | Is persistence and crash recovery implemented? | Narrow the claim; a lock alone is insufficient |
Read the diff, including tests and deleted code. A generated test can repeat the same mistaken comparison as the implementation. Derive the expected result from the requirement and the example, not from another call to the function being tested.
If a suggestion is wrong, say so calmly and explain the counterexample. That demonstrates judgment without turning the interview into a performance about the tool. The objective is a shared understanding of a correct change.
What to say while making the change
Make your uncertainty useful to the partner. Instead of “I think this is fixed,” say, “The exception already happens, but the balance changes before it. I will preserve the rejected-operation state and test it explicitly.” That statement gives the interviewer a chance to confirm your interpretation.
When feedback changes the requirement, summarize the revision before typing. “We now want the original response on retries, so I need to store the result as well as the request amount.” This keeps the conversation synchronized and shows why the data structure is changing.
If time becomes tight, prioritize a working behavior and its most important regression test. State which follow-up remains and why it matters. Do not describe unrun tests as passing or unfinished concurrency work as a guarantee. A precise handoff lets the interviewer distinguish deliberate scope from an overlooked failure mode.