A Software Engineer at the National Bank of Kuwait (NBK) plays a pivotal role in maintaining the digital infrastructure of one of the region’s most prestigious financial institutions. You are tasked with developing, securing, and optimizing the systems that facilitate critical banking operations, ensuring that both internal workflows and customer-facing digital services remain robust, scalable, and highly secure.
This role is inherently complex, as you will be working within a highly regulated environment where precision, security, and reliability are non-negotiable. You will contribute to the evolution of modern banking platforms, balancing the need for rapid technological innovation with the rigorous standards required by the financial sector. For the right candidate, this position offers the chance to work on high-impact projects that directly influence the financial stability and digital experience of millions of users.
The recruitment cycle at the National Bank of Kuwait is known for its thoroughness; candidates should be prepared for a process that requires patience and consistent follow-up to move through the various stages effectively.
Initial Screening
reportedThe person on this call usually cannot evaluate your code and does not need to. They write a short paragraph, and that paragraph is what a hiring manager skims when deciding who to put on your loop. So the test is not whether your work was hard, it is whether a non-engineer can repeat it correctly. Name systems by what they did rather than by their internal codename, give each project a shape (what was breaking, what you changed, what happened after), and keep the whole walkthrough near ninety seconds. Depth that cannot survive a paraphrase reads as vagueness.
What to demonstrate
- Whether a non-engineer can restate your projects without distorting them, since their paraphrase is what travels to the hiring manager, not your sentences
- Whether each project has a shape rather than a stack list: the failure or constraint, the change you made, the result and how it was measured
- Whether you can say what was yours inside a team project without either inflating it or disappearing into the plural
How to prepare
- Rewrite each headline project as two sentences with no internal system names and no acronyms outside your company, then say them to someone outside engineering and have them repeat them back. Fix whatever came back wrong
- Attach one measured number to each project: the baseline, the change, and the window it was measured over. Where nothing was ever measured, say that plainly rather than reaching for a plausible percentage
- Time the background walkthrough against a clock. If it runs past two minutes, compress the earliest role to a single clause and spend the recovered time on the most recent one
Formal Testing
reportedYou cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Technical Interviews
reportedWhat this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What to demonstrate
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Management Interviews
reportedAn unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.
What to demonstrate
- Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
- Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
- Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating
How to prepare
- Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
- Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
- Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes
PracHub editorial advice for the preparation topics above.
Writing the state change to the database and publishing the event in the same code path
No transaction spans a relational database and a message broker, so a crash between the two leaves one done and the other not, and the failure is asymmetric in both orderings: publish-then-commit invents events for state that never existed, while commit-then-publish silently loses events for state that does. Retrying the publish after the commit is not a fix, because the process can die before the retry runs. The working shape is an outbox row written inside the same transaction plus a relay that publishes it at least once, which makes consumer-side idempotency mandatory rather than optional. Note also that 'exactly-once' in a stream processor means exactly-once processing within that system's own read-process-write transaction, and says nothing at all about an external side effect such as charging a card.
Retrying a charge after a timeout
A timeout is not a failure; it is an unknown outcome, and the request may have been processed in full with only the response lost. Re-sending it without an idempotency key that the processor itself honours produces a duplicate charge, which is a customer-visible incident and usually a dispute. The correct handling is to treat the state as unknown, query the processor for that key or client reference, and only then decide. The mechanism also depends on the key being generated once by the caller and reused across every attempt — generating a fresh key per retry turns the whole scheme into a no-op while leaving all the code that appears to implement it in place.
Naming no test cases at all
State what you would test before being asked: empty input, a single element, all elements equal, the maximum permitted size, and the input that exercises the branch you just wrote. It costs thirty seconds and is much of what separates someone who has shipped code from someone who has only solved puzzles.
A cache with no invalidation story
Say how an entry goes stale, how long you can serve it stale, and what happens when many requests miss the same key at the same instant. One popular key expiring under load sends every concurrent request to the origin together; single-flight coalescing, jittered expiry, or serving stale while revalidating are the standard answers.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Answer as-of balance queries over an append-only entry log
Given 400 million ledger_entry rows (entry_id, account_id, direction, amount_minor, currency, business_date) and 2 million queries of (account_id, currency, as_of_date) asking for the balance at the end of that business date, produce every answer. The obvious solution — per query, sum that account's entries with business_date <= as_of_date — is correct. Say precisely why it will not finish, then give one that will, with time and space complexity. Corrections are posted as new entries carrying their own business_date.
Approach
- Cost the naive version in numbers before rejecting it. Spread uniformly over 20 million accounts, each query touches about 20 rows behind a per-account index and 2 million queries is 4e7 row touches — perfectly fine. The problem is skew: one pooled clearing or merchant settlement account holding 3e7 entries, taking 10% of the queries, is 6e12 row touches. Name the skew; 'n is large' is not the reason.
- The structural fact that buys a cheap answer: entries are append-only and never updated, so a prefix sum over an account's entries ordered by
(business_date, entry_id)is stable — nothing behind position i can change. No mutable-balance design offers that, and it is why the storage is worth paying for. - Offline sweep, when all queries are known up front: externally sort entries by
(account_id, currency, business_date, entry_id)and queries by(account_id, currency, as_of_date), then merge-walk both with a running sum, emitting each query's answer as the sweep passes its date. O((n + q) log(n + q)) dominated by the sort, O(1) beyond sort buffers, one sequential pass over each input instead of 2 million random seeks. - Online alternative: materialise end-of-day snapshots — one row per
(account_id, currency, business_date)that had activity, holding the cumulative total. A query becomes one index seek for the latest snapshot at or beforeas_of_date, O(log n) per query, over far fewer rows than n. Use snapshots when queries arrive singly and the sweep when they arrive as a batch. - Corrections are the subtlety: an entry posted today but dated back changes historical answers, so every snapshot for that account from that date forward is stale. Either keep a Fenwick tree over dates per account (O(log D) update and prefix query) or recompute that account's snapshots from the corrected date onward. Then be precise about what reproducibility means — yesterday's statement is reproducible as of a stated snapshot time, not identical forever.
- Bound the resources: int64 sums throughout, no float; 400 million rows at roughly 48 bytes of the columns you actually need is about 19 GB, so the sort is external and its fan-out is chosen from the sort buffer, not from the row count.
Follow-up
- One account holds 30% of all entries. What does the external sort do with it, and what would you do for that one key instead?
- Queries now arrive online at 500 per second. Which design survives, and what does keeping the other one warm cost?
- A correction lands with a
business_date90 days back. Which snapshots are now wrong, and how does a reader find out?
Resolve party identity across merges without rewriting posted history
Duplicate parties get merged over time: a stream of up to 5 million (loser_party_id, winner_party_id, merged_at) events, deliverable in any order, sometimes repeating a pair already merged. Ten million account rows and hundreds of millions of ledger_entry rows already reference pre-merge party ids and must not be rewritten. Build the structure that answers 'what is the canonical party for this id?' in near-constant time, and explain how a report that groups by party stays correct without touching a single historical row.
Approach
- The merge events form an undirected graph over party ids and the question is connected components, so use disjoint-set union with union by size and path compression:
findandunionamortise to O(alpha(n)), and alpha(n) is at most 4 for any n that exists. Five million merges over ten million ids is two integer arrays and a few hundred milliseconds. - The domain twist: the representative cannot be whichever root union-by-size happens to pick, because the canonical party is a business decision recorded as
winner_party_id. Keep union by size for the tree shape and a separateroot -> canonical_party_idmap for the label. Conflating them makes the canonical id flip when an unrelated merge reshapes the tree, and every report built on it moves retroactively. - Never rewrite
ledger_entryoraccount. Resolve at read through the canonical map: rewriting posted rows destroys the audit trail, locks hundreds of millions of rows, and buys nothing the map does not already give you. - Materialise the map fully expanded as
party_id -> canonical_party_id, one row per member rather than per edge, so a report is a single join instead of a recursive traversal. Rebuild it from the event log, which keeps it a derived artefact that can be regenerated rather than a second source of truth that drifts. - Pin down determinism: a repeated merge is a no-op because both ids already share a root, but two events merging the same pair in opposite directions is a genuine conflict. Write the tiebreak down — lowest
merged_at, then lowest id — or two replays of the same log produce different labels for the same data. - Un-merges: disjoint-set union has no delete. If a merge can be reversed, keep the event log authoritative and rebuild the whole structure from the surviving events; at 5 million events that is seconds, and far cheaper than any incremental un-union scheme.
Follow-up
- A merge is reversed after six months of postings. What exactly gets rebuilt, and what does the statement produced last month now say?
- Two processes apply merge events concurrently. What guarantees they converge on an identical canonical map?
- Resolving at read on the hot path versus materialising the map — where does that join actually live, and what does each cost?
Parse settlement amounts into minor units without floating point
A settlement file carries amounts as text: 1234.56, -0.07, 1,234.5, 1234, 12 345,67, and for three-exponent currencies 1.234. Each line also carries an ISO 4217 alphabetic code and the file's sign convention. Write the function converting one amount string plus its currency into a signed amount_minor int64, scaling by the currency's ISO 4217 exponent — 0 for JPY and KRW, 2 for USD and EUR, 3 for KWD and BHD. Reject anything not convertible exactly, with a typed reason. Never raise, never use a float.
Approach
- Fix the contract first: return a result union of
Ok(int64)orErr(reason)with reason drawn fromunknown_currency,malformed,too_many_fraction_digits,overflow. A parser on this path that throws turns one bad line into a failed fifty-million-line run. - Decide which character is the decimal mark from the file's format spec, not from a heuristic on the last separator, then strip grouping separators.
1,234is genuinely ambiguous between 1234 and 1.234 exactly when the exponent is 3, so an unstated spec is a reject, not a guess. - Split on the decimal mark. Right-pad the fraction with zeros to the currency's exponent e; if it is longer than e and any dropped digit is non-zero, return
too_many_fraction_digits. Silently truncating is how half a minor unit becomes a reconciliation break nobody can source. - Build the integer by digit accumulation on int64 with checked multiply and add, or equivalently concatenate the integer and padded-fraction substrings and parse once. Either way the value never passes through a float.
- Apply the sign last, from the convention the file declares — trailing
CR/DR, a trailing minus as in1234.56-, or a leading minus. Assuming a leading minus silently flips every credit in a file that uses the trailing form. - Complexity is O(len) per amount with no allocation beyond a digit buffer, so the whole file is one O(total bytes) pass.
Worked solution 20 min
- Write the exponent table
{JPY:0, KRW:0, USD:2, EUR:2, KWD:3, BHD:3}and the result type before any parsing code. - Implement integer-only scaling: split on the decimal mark, pad or reject the fraction, concatenate, parse once with checked arithmetic.
- Table-test: (
1234.56, USD) to 123456; (1234, JPY) to 1234; (1.234, KWD) to 1234; (1234.56, JPY) toErr(too_many_fraction_digits); (0.005, USD) toErr; (-0.07, USD) to -7. - Write the float implementation beside it and run both over ten thousand random amounts across all six currencies, printing every disagreement.
- Round-trip: format
amount_minorback to text at the currency's exponent and assert equality with the normalised input.
Follow-up
- Exponent 4 exists (CLF). Does the exponent come from a compiled-in table or from the file header, and what happens when the two disagree?
- A line has more fraction digits than the exponent allows. Is that a reject, a round, or a break — and who makes that call?
- How do you fuzz this so that every failure is a typed rejection rather than a wrong number?
Stop the capture-and-refund join from double-counting merchant money
Report captured_minor, refunded_minor and net_minor per merchant per business_date. payment_intent holds intent_id, merchant_id, amount_minor, captured_minor, refunded_minor. ledger_entry holds transaction_id, account_id, direction, amount_minor, source_type in (capture, refund, fee, chargeback), source_id = the intent_id as text, and business_date; every capture posts one debit and one credit. A draft joins payment_intent to ledger_entry twice, once filtered to captures and once to refunds, and sums both. On intents with two partial captures and one refund the totals are wrong. Name both multiplications precisely and write the correct query.
Approach
- Name the first multiplication by cardinality: intent to capture entries is one-to-N and intent to refund entries is one-to-M, so joining both yields N times M rows per intent. Every capture row is then counted M times and every refund row N times, and the two columns are inflated by different factors, which is why the totals look almost right rather than obviously broken.
- Name the second, which is easier to miss: double-entry means each capture posts two legs, so summing every entry of a capture counts the amount twice regardless of joins. Select the single leg by ACCOUNT - the merchant's settlement payable, which carries exactly one leg of every capture and of every refund - and not by direction. Direction is not a substitute for that filter: a capture credits the payable and a refund debits it, so adding direction = 'credit' keeps the captures and silently drops every refund leg, leaving refunded_minor at exactly zero on a query that otherwise reads as correct.
- Reject the reflexive fixes: SELECT DISTINCT and SUM(DISTINCT ...) de-duplicate values, not rows, so two genuine captures of equal amount collapse into one and the total is wrong in the other direction while looking tidier.
- Prefer a single pass with conditional aggregation over ledger_entry, restricted to that one account: SUM(amount_minor) FILTER (WHERE source_type = 'capture') and the same for refunds, grouped by merchant and business_date, joining payment_intent only to reach merchant_id. One scan, no intent-level fan-out, and fees or chargebacks are added as another FILTER rather than another join. amount_minor is a positive magnitude, so net_minor is captured minus refunded; the signed form over the same rows, SUM(CASE WHEN direction = 'credit' THEN amount_minor ELSE -amount_minor END), returns the same net in one column and is the check on the sign convention.
- Keep aggregate-then-join in reserve for when per-intent detail is required: aggregate each side to intent grain in its own CTE, then join the two aggregates one-to-one. LATERAL works too and reads better when the right side needs the left's key.
- Get the account predicate, the join column and the date right. Each merchant has its own settlement payable, so the predicate is a join to the account dimension (a.account_type = 'merchant_payable' AND a.merchant_id = pi.merchant_id); a single $settlement_account literal is only correct for a one-merchant report. source_id is text and intent_id is uuid, and PostgreSQL has no implicit cast between them, so one side must be cast and the side decides which index stays usable - pi.intent_id = e.source_id::uuid probes payment_intent's primary key per entry row, e.source_id = pi.intent_id::text probes an index on ledger_entry(source_id), and if neither fits the driving table the planner hashes both sides, which is fine for a day's report and not for a single-intent lookup. Group on ledger_entry.business_date rather than posted_at::date, because they differ across the cutoff, and decide explicitly whether a later-dated refund reduces its own day or the original capture's.
Follow-up
- A refund lands on a later business_date than its capture. Which day does net_minor move, and what does that do to a daily merchant payout?
- Add scheme fees, which arrive netted into a batch total rather than per transaction. How does the query change, and what can it no longer claim?
- How would you reconcile this output against payment_intent.captured_minor, and which one is authoritative when they disagree?
Store multi-currency amounts and prove each transaction balances
ledger_entry holds entry_id, transaction_id, account_id, direction (debit or credit), amount_minor (bigint, CHECK > 0), currency char(3), source_type, source_id, business_date and posted_at. The service posts in JPY, USD and KWD. Specify the column types for money and the reference table that carries each currency's ISO 4217 minor-unit exponent, and say why that exponent must not be a constant in application code. Then write the query that returns every transaction_id whose entries fail to sum to zero within each currency, ordered by the largest absolute imbalance.
Approach
- State the representation: amount_minor is an integer count of minor units and the scale lives with the currency, not in the column. int4 tops out at 2,147,483,647 minor units, which is about 21.5 million units of a 2-exponent currency, so bigint, and the currency code travels on every row that carries an amount.
- Normalise the scale into currency(code char(3) primary key, exponent smallint, name) and join it only at display and parse boundaries. The exponent is 0 for JPY and KRW, 2 for USD and EUR, 3 for KWD, BHD, JOD, OMR and TND, so a hard-coded divide by 100 is wrong by 100x for JPY and by 10x for KWD, in opposite directions.
- Rule out binary floating point outright: IEEE 754 binary64 cannot represent 0.1, so repeated accrual drifts by a few minor units that later appear as reconciliation breaks. If a fixed-scale decimal is used instead, pin the scale per currency and name the single place rounding happens.
- Write the balance check as a grouped aggregate: sign the amount with a CASE on direction, GROUP BY transaction_id, currency, and HAVING the signed sum <> 0. The sign belongs in direction, and the CASE is the one place it becomes arithmetic.
- Then order in a second layer, because the ordering the question asks for cannot be written on the grouped query itself. PostgreSQL lets an output alias stand alone in ORDER BY but resolves anything inside an expression against the input columns, so ORDER BY abs(imbalance_minor) DESC raises SQLSTATE 42703, column "imbalance_minor" does not exist, while the bare ORDER BY imbalance_minor DESC is accepted and sorts signed rather than absolute. Wrap the grouped query and order by abs() outside it, or repeat the aggregate as ORDER BY abs(SUM(CASE ...)) DESC, which is legal because aggregates are allowed there.
- Group per (transaction_id, currency) rather than per transaction, because a cross-currency movement balances only within each currency, bridged by an explicit FX position account whose rate, source and timestamp are stored on the transaction.
- Note that no CHECK constraint can express this, since it spans rows: enforce it at write time with a DEFERRABLE INITIALLY DEFERRED constraint trigger that fires at commit, or by routing every posting through one function, and keep this query as the independent audit.
Worked solution 15 min
- Define currency(code, exponent, name) and seed JPY 0, USD 2, KWD 3; declare amount_minor bigint NOT NULL CHECK (amount_minor > 0) and currency char(3) REFERENCES currency(code).
- Write it in two layers so it runs: SELECT transaction_id, currency, imbalance_minor FROM (SELECT transaction_id, currency, SUM(CASE WHEN direction = 'credit' THEN amount_minor ELSE -amount_minor END) AS imbalance_minor FROM ledger_entry GROUP BY transaction_id, currency HAVING SUM(CASE WHEN direction = 'credit' THEN amount_minor ELSE -amount_minor END) <> 0) b ORDER BY abs(imbalance_minor) DESC.
- Post one clean capture, one deliberately unbalanced transaction, and one cross-currency transfer with an FX position account, then run the query.
- Convert 1000 JPY and 1000 KWD through the exponent table in both directions and confirm the round trip is exact.
Follow-up
- A transaction has a USD leg and a JPY leg. Which accounts appear, and where does the rounding residual land?
- How would you enforce the zero-sum rule at write time without serialising all postings behind one lock?
- Someone proposes storing a display amount alongside amount_minor. What goes wrong at the first rate change or rounding rule change?
How do you approach technical obstacles during the development lifecyc…
How do you approach technical obstacles during the development lifecycle?
Approach
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Describe cookies and explain why they are used.
Describe cookies and explain why they are used.
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
How do you ensure data integrity within your application architecture?
How do you ensure data integrity within your application architecture?
Approach
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Raise the write ceiling on one hot clearing account
The ledger posts balanced entry sets at 10,000 entries/s into append-only ledger_entry(entry_id, transaction_id, account_id, direction, amount_minor>0, currency, source_type, source_id, business_date, posted_at), roughly three entries per transaction. A materialised row per account carries the floor check available_minor >= floor_minor and is updated in the posting transaction. One pooled clearing account appears in 40 percent of transactions. Compute the throughput ceiling on that one row, then raise it. Name the isolation level each version requires and state precisely what sharding the balance costs the floor check.
Approach
- Compute the ceiling before designing anything. Throughput against a single row is one divided by the lock hold time, where the hold runs from first acquiring the row to commit, including the WAL flush. At a 2 ms hold that is about 500 commits/s on that row, and adding cores does not move it. Demand is 10,000 entries/s at three entries per transaction, so roughly 3,300 transactions/s, of which 40 percent touch this account: about 1,300/s against a 500/s ceiling. You are 2.6x over before any code exists.
- Shorten the hold before sharding, because it is free. Acquire the hot row last in the transaction and hold it for exactly one statement: UPDATE balance SET available_minor = available_minor - $1 WHERE account_id = $2 AND available_minor - $1 >= floor_minor. This is correct at READ COMMITTED, because an UPDATE that blocks on a concurrent writer re-evaluates its WHERE clause against the newly committed row version rather than its original snapshot.
- Name what the broken version needs. SELECT the balance, compute in application code, then UPDATE loses updates at READ COMMITTED, since the second statement takes a fresh snapshot. Making that shape safe requires REPEATABLE READ or SERIALIZABLE plus a bounded retry loop on SQLSTATE 40001, which is the part most implementations omit; SELECT ... FOR UPDATE also works but is simply the same lock held for longer. Note that the identical application code changes behaviour on a different engine: InnoDB's REPEATABLE READ does not abort the loser.
- Order locks deterministically on any two-account movement, always ascending by account_id. Concurrent opposing transfers without that ordering deadlock with SQLSTATE 40P01, and the deadlock rate rises with exactly the concurrency you added to gain throughput.
- Shard the hot balance into N sub-rows keyed (account_id, shard_no), writers choosing a shard by hash of transaction_id. Per-shard hold time is unchanged, so capacity rises roughly N-fold, and at N=16 you clear 1,300/s with headroom. The cost is precise: the floor is now a predicate over the sum of N rows and is no longer expressible as a single-row CHECK, so it must be paid for deliberately. Either give each shard its own floor, which rejects postings while other shards hold headroom, or add a slow path that locks all shards in ascending shard_no under SERIALIZABLE, rebalances and retries on 40001. For a clearing account whose floor is effectively unreachable, dropping the per-shard floor and enforcing it as a monitored invariant is a defensible stated choice; for a customer deposit account it is not.
- Keep the entries immutable throughout: no UPDATE or DELETE grant on ledger_entry plus a trigger, corrections as reversing transactions, and partitioning by business_date so index writes stay in the current partition and retention is a DETACH.
Worked solution 45 min
- Run two concurrent sessions doing SELECT then UPDATE to a computed value against the same account under READ COMMITTED and observe one update lost.
- Replace it with the single atomic conditional UPDATE and re-run; then run the original read-modify-write under REPEATABLE READ with a bounded retry on 40001.
- Measure committed transactions per second against the single pooled row at 1, 8, 32 and 128 concurrent writers, recording mean lock hold time.
- Shard into 16 sub-rows hashed on transaction_id, re-measure, and write the query that answers the true available balance.
- Run concurrent opposing two-account transfers with and without ascending account_id lock ordering.
Follow-up
- While the balance is sharded, what query answers the account's true available balance, and what does it cost?
- A transaction has legs in two currencies. What does the balanced-to-zero check mean now, and where does the rounding residual go?
- What does the nightly reconciliation compare the sharded balance against?
Event gateway heap grows until the daily restart
The outbound event gateway holds RSS at 1.2 GB for about nine hours, then climbs to its 4 GB limit and is OOM-killed. It runs per-destination queues with bounded concurrency and a retry schedule extending to 24 hours. Event rate, destination count and throughput are flat across the window. Heap dumps show the largest retained set is a map keyed by destination id. Give the ordered diagnostic checklist and the cause, and distinguish a leak from growth that is merely unbounded.
Approach
- Separate the two possibilities first, because they need different fixes and look identical on an RSS graph. A leak retains objects unreachable from any live work; unbounded growth retains objects that are genuinely still needed because nothing caps how many there are. The discriminator is whether the retained set corresponds to real pending work, so count the map's entries against the destination count and its values against in-flight events.
- Read the dominator tree, not the instance histogram. A histogram says byte[] is large, which is always true. The dominator tree says what holds it. A destination-keyed map with more entries than destinations means per-destination state is created and never removed, which is a leak; a map with exactly the destination count whose values are large means per-destination state grows without bound, which is a design limit.
- Follow the retry schedule to the worst case. A 24-hour schedule with at-least-once delivery means an event for a dead destination is retained for a day, so an unbounded in-memory queue accumulates a day of that destination's traffic. Compute it explicitly: events per second for the busiest destination times 86,400 times average payload size, then compare against the 2.8 GB of headroom. If it lands in the same order of magnitude, the design is unbounded rather than leaking.
- Use the shape of the curve to date the trigger. RSS flat for nine hours and then climbing does not mean slow accumulation all along; it means accumulation started at a point in time, so find what changed at hour nine rather than what the process does continuously. Measure the slope of the climbing segment alone, convert it to retained bytes per second, and compare it against one destination's event rate times payload size: a match to a single destination points at a specific endpoint that went unreachable then, while a match to the total points at a global retention path such as a delivered-event cache or an unbounded metrics label set.
- Fix by bounding, not by enlarging. Give each per-destination queue a maximum depth and a maximum age, and spill beyond it to outbox_event, which is durable, already exists and already carries aggregate_version for ordering. Memory then scales with in-flight concurrency rather than with backlog, and a destination down for a day costs disk, not heap.
- Prove it rather than watching it. Re-run with one destination black-holed and assert RSS stays inside a stated band for an hour. A fix that only raises the limit passes a short soak and still fails, just later, which is why the test has to inject the failure rather than wait for it.
Follow-up
- Spilling to the table means some events for a destination are in memory and some are in PostgreSQL. How do you keep per-destination order across both?
- The bounded queue is full and the event is already committed in the outbox. What does the enqueue path do, and what does it return?
- Which single metric would have caught this at hour two rather than hour nine?
Four days sample coding, design, fundamentals and the practical rounds at deliberately shallow depth, which is enough to surface the topics you did not know were in scope. That map, rather than a guess made on day one, decides where the last three days go.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Coding, one pass at shallow depth
- Solve one problem from each of six families, an array with two pointers, hash counting, binary search, a tree traversal, a graph traversal and one dynamic program, under a hard twenty-minute cap with no extensions, marking each finished, late, or stalled.
- For every stall, write the exact move you could not make rather than the subject, so the note reads could not turn the recurrence into a loop rather than bad at dynamic programming.
- Fix nothing today. The value of the pass is the unfixed record.
Deliverable: Six timed attempts marked finished, late or stalled, each stall carrying a named blocking move.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Design, one pass at shallow depth
- Spend twenty minutes each on three different shapes, a read-heavy feed, a write-heavy ingest path, and something needing a transaction across two entities, stopping each at requirements, interface and data model.
- After each, write the first question you could not answer, which is usually a number you could not estimate or a failure mode you had no vocabulary for.
- Mark which of the three you would be most relieved not to be asked, and treat that as data rather than as a preference.
Deliverable: Three shallow designs, each with the first unanswerable question written at the bottom.
Practice prompt ↗Practice prompt ↗03Fundamentals and the practical rounds
- Answer eight short questions in writing at four minutes each, covering the material that fills the gaps between the big rounds: what happens between a URL and a rendered page, what an index costs on write, when a process is preferable to a thread, and what conditions a deadlock requires.
- Do one thirty-minute practical task of the kind a take-home compresses: read an unfamiliar two-hundred-line file and write what it does, what you would change, and the one thing you remain unsure of.
- Score every answer fluent, correct but slow, or absent, and keep the absent ones visible.
Deliverable: Eight scored short answers and one written reading of unfamiliar code.
Practice prompt ↗Practice prompt ↗04The rounds that are about you, and the map
- Deliver three behavioural answers aloud against a timer, a conflict, a failure you owned, and a decision made without enough information, marking any that ran past three minutes or contained no number.
- Assemble the map: every marked item from days one to three on a single page, sorted by how likely it is to appear in your loop rather than by how uncomfortable it felt.
- Choose exactly two areas for the remaining three days and write down what you are deliberately abandoning.
Deliverable: A one-page scored map of the whole surface area with two areas chosen and the rest explicitly abandoned.
Practice prompt ↗Practice prompt ↗Worked solution ↗05First chosen area, to the depth you skipped
- Work the higher-ranked area in four focused blocks, choosing items one level above where you stalled rather than repeating what already works.
- After each block write the rule you extracted in one sentence with its precondition attached, since a rule carrying no precondition is exactly what fails under a variation.
- Re-attempt the day-one or day-two item that exposed this area and compare against the original timing.
Deliverable: Four worked blocks, a timed re-attempt against the original, and three one-sentence rules with preconditions.
Practice prompt ↗Practice prompt ↗06Second chosen area, where the gap is coverage rather than speed
- Treat the second area differently from the first. Day five drilled something you could already half-do; this one is usually a topic you had simply never met, so build one worked reference example end to end and keep it, rather than attempting six problems badly.
- Write down the vocabulary you were missing on day two or three, five terms at most, each with the one sentence that makes it usable in an answer rather than the textbook definition.
- Redo the shallow attempt that exposed this area and note whether you now fail later in the problem, because moving the failure point is the realistic gain from a single day and is worth more than a score that did not change.
Deliverable: One worked reference example for the newly covered area, a five-term vocabulary list, and a note on where the failure point moved.
Practice prompt ↗Practice prompt ↗07Reassemble the loop
- Sit two rounds back to back with no gap, ordering them so the area you chose second comes last, because the map was built from rested, isolated attempts and the loop will reach your weaker area when you are already spent.
- Write where the second round suffered from the first, which is normally the point at which structure collapses into narration.
- Reduce the week to one page holding only the rules you can state without reading them.
Deliverable: Mock notes on cross-round carryover plus a one-page card of rules you can recite from memory.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
A slipped date is only a bad story if you sat on it. What matters is what you believed when you gave the number, the signal that told you it was wrong, how many days passed before you said so, and what you cut rather than asking for more time. Scope you defended counts as much as scope you dropped.
How do you handle high-pressure environments when working on critical …
How do you handle high-pressure environments when working on critical system updates?
Approach
- Pick a story where you made the decision, not one where you watched it.
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Own the postmortem for a duplicate-capture incident
A processor slowed down, callers timed out and retried without reusing their idempotency key, and 412 captures were duplicated over 90 minutes before a reconciliation break report surfaced it. Take the on-call role. Describe an incident you owned of comparable blast radius: how it was detected, how you bounded the affected population, what you stopped first, and how customers were made whole. Give a wall-clock timeline, the query or metric that sized the damage, and the change that would have prevented it. Include what you got wrong during the response, not only after it.
Approach
- The probe is whether you can bound an unknown blast radius under time pressure. Open with the invariant that broke (at most one capture per authorisation attempt) rather than the symptom, because the invariant tells the listener what to count.
- Size the population with a stated query, not an adjective: duplicate captures are ledger_entry rows with source_type='capture' grouped by source_id having count(*) > 1, joined back to payment_intent for the affected merchants and amounts. Say how long that query took and whether you could run it against a replica while the incident was live.
- Separate mitigation from fix and say which you did first. Mitigation is usually cheap and blunt (disable the retry path, drop the caller's concurrency, hold captures behind a flag); the fix is a UNIQUE constraint plus a stored response, and it is not an incident-window change.
- State the remediation arithmetic explicitly: refunds are new customer-visible movements with their own fees and their own settlement lag, so the count of duplicates, the total minor units, the refund posting date and the customer notification are four separate numbers a strong answer has ready.
- Close on the prevention change and its cost. Naming one guard that would have caught it earlier (a break-age alert, a duplicate-capture counter on the ledger write path) beats listing five that nobody staffed.
- Name your own error inside the response window: a mitigation you tried that made it worse, or the 20 minutes you spent on the wrong hypothesis. Interviewers weight that heavily because it is the part candidates rehearse away.
Follow-up
- The retry came from a client you do not control. What do you change so a client that regenerates its key per attempt cannot cause this again?
- How would you have detected it in 5 minutes instead of 90, and what would that detector cost in false pages per week?
- A merchant disputes your count of affected transactions. What do you show them?
Resolve a review disagreement over isolation level
A colleague's pull request reads a balance, compares it to a floor in application code, then issues an UPDATE with the computed value, all under PostgreSQL's default READ COMMITTED. You comment; they reply that staging has never produced a negative balance. Describe a code review disagreement where you were confident and the author was not convinced. State how you made the failure concrete rather than theoretical, how many rounds it took, at what point you would have escalated or approved anyway, and what you conceded to the author.
Approach
- The probe is whether you can convert a correctness objection into something reproducible instead of a stalemate of opinions. Name the anomaly by its mechanism: under READ COMMITTED each statement takes a fresh snapshot, so two sessions can both read balance 100, both compute 100 minus 80, and both write 20.
- Address the staging evidence directly rather than dismissing it. Staging concurrency on one account row is effectively one, so the absence of the anomaly there is expected under both the broken and the correct implementation. That is the sentence that usually ends the argument.
- Reproduce it in two psql sessions and paste the interleaving into the review. A twelve-line transcript settles in one round what three paragraphs of theory will not settle in four.
- Offer the fix as a choice with its trade-off, not as a verdict: an atomic UPDATE ... SET balance = balance - $1 WHERE account_id = $2 AND balance - $1 >= $3 with a rowcount check keeps it single statement and needs no retry; SELECT ... FOR UPDATE serialises the row and lets you compute in application code; SERIALIZABLE covers the multi-row version of the predicate but requires a bounded retry on SQLSTATE 40001 that someone has to actually write.
- Say where your bar is. Correctness on money is a blocking comment, style is not, and a strong answer states that boundary before the disagreement rather than discovering it during one.
- Name what you conceded. The author was usually right about something (scope, naming, the follow-up being separable), and saying so is what makes the blocking comment land next time.
Follow-up
- The author switches the service to MySQL. Which of the three fixes still behaves the same, and which changes silently?
- The same endpoint later transfers between two accounts. What do you now require in the review?
- How do you keep this from being relitigated in every future pull request?
- 01
How do you handle high-pressure environments when working on critical system updates?
- 02
A processor slowed down, callers timed out and retried without reusing their idempotency key, and 412 captures were duplicated over 90 minutes before a reconciliation break report surfaced it. Take the on-call role. Describe an incident you owned of comparable blast radius: how it was detected, how you bounded the affected population, what you stopped first, and how customers were made whole. Give a wall-clock timeline, the query or metric that sized the damage, and the change that would have prevented it. Include what you got wrong during the response, not only after it.
- 03
A colleague's pull request reads a balance, compares it to a floor in application code, then issues an UPDATE with the computed value, all under PostgreSQL's default READ COMMITTED. You comment; they reply that staging has never produced a negative balance. Describe a code review disagreement where you were confident and the author was not convinced. State how you made the failure concrete rather than theoretical, how many rounds it took, at what point you would have escalated or approved anyway, and what you conceded to the author.
Is this an official National Bank of Kuwait interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at National Bank of Kuwait. Rounds and questions reflect what candidates have reported, not a process National Bank of Kuwait has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long does the entire interview process usually take?
The process can be quite long and may span several months. It is important to remain patient and maintain professional communication with the recruiting team throughout.
PracHub interview research ↗What is the best way to prepare for the technical interview?
Focus on your past projects and be ready to explain the architecture and the specific technical choices you made. Reviewing fundamental web concepts like session management is also highly recommended.
PracHub interview research ↗Is it okay to follow up on the status of my application?
Yes. In fact, given the nature of the process, it is expected that you remain proactive. Checking in periodically shows your genuine interest and helps keep your application moving.
PracHub interview research ↗What is the culture like at the National Bank of Kuwait?
It is a professional and structured environment. Success here is often tied to your ability to work within established protocols while delivering reliable and secure software.
PracHub interview research ↗Sources & methodology 3 sources ↗
No official company page is cited. Rounds and questions come from candidate reports and PracHub editorial material; each source shows the date it was read.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
PracHub page · Accessed 2026-09-22 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
PracHub page · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
PracHub page · Accessed 2026-09-22