As a Software Engineer at Lloyds Banking Group, you are at the heart of the digital transformation of one of the UK’s most established financial institutions. Your work directly impacts millions of customers, ensuring that the banking services they rely on—from mobile apps to complex transaction processing systems—are secure, scalable, and intuitive. You will join a high-performing engineering community tasked with modernizing legacy infrastructure while pioneering new, cloud-native financial products.
This role requires more than just technical proficiency; it demands a deep understanding of the regulatory and security-conscious environment of the banking sector. You will collaborate across multidisciplinary teams, including product owners, architects, and data specialists, to deliver robust solutions that balance rapid innovation with the stability required in financial services. Whether you are optimizing a microservice architecture or enhancing the security of a CI/CD pipeline, your contributions are critical to maintaining the trust and reliability that define the Lloyds Banking Group brand.
Expect to work in an environment that prioritizes "Engineering the Lloyds Way," a framework emphasizing collaboration, continuous improvement, and customer-centric design. You will be challenged to solve complex problems at scale, often bridging the gap between cutting-edge technology and established enterprise standards. It is a position of significant responsibility where your technical decisions have a measurable influence on the financial well-being of the bank’s diverse customer base.
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
Technical Assessments
reportedThe same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
Behavioral or Panel Interviews
reportedThis round is deciding whether a change you make without supervision can be allowed to reach production. It is scored on what you knew at the moment you decided, not on how it turned out, so a story that opens with the result and works backwards reads as luck retold as judgement. Say what the options were, what you did not know, what you did to shrink the unknown before committing, and what you accepted as the worst plausible case. The detail that separates answers is a bound: how many users, how much data, and for how long, if you had been wrong.
What to demonstrate
- Whether the reasoning you give was available at the time you decided rather than after the result came in, since a story whose deciding evidence arrived later describes an outcome and not a judgement
- Whether you can put units on the exposure (users, rows, minutes of degraded service) and whether the containment you chose actually bounded it: a canary bounds the request path it fronts, while a background job writing to a shared table reaches every user regardless of which version served their requests
- Whether the reversal path existed before you shipped or was improvised during the incident, and whether it restores state or only stops further damage
How to prepare
- For your three largest changes, write down the one thing you would have had to be wrong about for it to fail, and what your best estimate of it was on the day you shipped. If you never held an estimate, that is the gap the follow-up questions will find
- Write the undo procedure for one of those changes as it existed at the time, then mark which steps restore data and which only stop new damage. Turning a flag off or reverting a deploy ends the new writes; rows already written come back only from a copy you kept, and a dropped column comes back empty unless something outside the schema holds the values
- Rehearse one story from the decision point forward and stop before the outcome, then have someone ask what you would do next. If the story only works with the ending attached, it is an anecdote rather than a decision you can defend
PracHub editorial advice for the preparation topics above.
Treating money as a decimal with two places
ISO 4217 exponents are 0 for currencies such as JPY and KRW, 2 for most, and 3 for BHD, KWD, JOD, OMR and TND, so a hard-coded multiply-by-100 is off by a factor of 100 or 10 depending on the currency, in opposite directions. Floating point is worse: IEEE 754 binary64 cannot represent 0.1 exactly, so repeated accrual accumulates drift that appears as a handful of minor units in the daily reconciliation and then gets 'fixed' by widening the match tolerance, which is how a genuine break becomes invisible. The only forms that survive a reconciliation are integer minor units with the exponent carried alongside the currency code, or a fixed-scale decimal type with exactly one documented rounding point.
Deriving the business date from the UTC timestamp
Posting date, value date and the processor's settlement date are three different dates, determined by cutoff times, business-day calendars and holidays rather than by midnight UTC. A movement recorded at 23:50 on one side of a cutoff belongs to the next business date, so computing business_date as created_at::date makes daily totals disagree with every statement and every settlement file. The signature is a reconciliation break that resolves itself the following day and then reopens, which reads like a flaky job and is actually a data model that is missing a column: business_date has to be stored explicitly and set from the cutoff rule, with the timestamptz kept separately for ordering.
Issuing one query per row of a result set
Fetch related rows in a single batched query keyed by the ids you already hold, or join them into the original query. A per-row round trip multiplies network latency by the row count, and it looks perfectly fine against the ten rows in your development database.
Tests that assert on the implementation rather than the behaviour
Assert on what a caller can observe, not on the number of internal calls or the shape of a private field. A test that breaks on every refactor but still passes when the answer is wrong costs more than it protects.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Rank merchants by unmatched break value in one streamed pass
A reconciliation run streams up to 50 million break rows of (merchant_id, currency, delta_minor signed, break_type). Return the 50 merchants with the largest total absolute delta_minor in a single currency, from one pass, with memory that does not grow with merchant count — there are up to 8 million distinct merchants and room for roughly 200,000 counters. Give both an exact and an approximate design, state the error bound the approximate one actually guarantees, and say which you would put in the nightly job.
Approach
- Start by testing whether the constraint is real. Exact needs one hash map
merchant_id -> int64plus a size-50 min-heap: O(n) to aggregate, O(m log 50) to rank, O(m) space. At 8 million merchants and 16 bytes of payload that is a few hundred megabytes — quote the number before reaching for a sketch. - If it genuinely does not fit, exact still costs only two passes or an external group-by: partition by
hash(merchant_id) % Pto disk, aggregate each partition independently, then merge with a K-way heap. This is the answer whenever exactness is non-negotiable, which for money it usually is. - One-pass approximate: weighted Space-Saving with C counters. An item either hits an existing counter or evicts the minimum one and inherits its value as an over-estimate. With total weight W and C counters, every reported total over-estimates by at most W/C, and any merchant whose true total exceeds W/C is guaranteed to be in the table. Quote W/200,000 as a fraction of the day's break value.
- State what the bound does not give you: the ordering within the top 50 is not guaranteed, and a merchant just below the threshold can be missed entirely. The honest claim to an operations reader is 'contains everyone above 0.0005% of today's break value', not 'the top 50'.
- Decide the metric before either design, because
sum(abs(delta))andabs(sum(delta))are different questions: a merchant with offsetting +1,000,000 and -1,000,000 breaks is top-ranked under the first and invisible under the second. Break work wants the first. - Recommend exact (two-pass or external group-by) for the nightly job — it runs once, has hours of budget, and an analyst acts on its output. Keep the sketch for a live dashboard, where a cheap bounded approximation beats an exact number that is four hours stale.
Worked solution 25 min
- Fix the metric in writing as
sum(abs(delta_minor))per(merchant_id, currency), because that is what an analyst works from. - Implement exact: hash-map aggregate, then a size-50 min-heap — push while the heap is under 50, then push-pop only when an item beats the root.
- Implement weighted Space-Saving with 200,000 counters over the same stream.
- Generate 50 million rows with a Zipfian merchant distribution near s = 1.1 so a few merchants dominate, then generate a uniform variant for contrast.
- Compare: how many of the exact top 50 the sketch recovers, and the largest observed over-estimate, against the W/C bound.
Follow-up
- Totals are now needed per currency as well as overall. What does that do to the key and to the counter budget?
- Prove the Space-Saving over-estimate bound to me in two sentences.
- The nightly job restarts halfway. Is the aggregate idempotent, and what does the heap do with a partially consumed stream?
Derive per-account balances and catch unbalanced transactions
You are given ledger_entry rows streamed in entry_id order: transaction_id, account_id, direction (debit or credit), amount_minor (a positive int64), currency, business_date. Up to 500 million rows, at most 20 million distinct (account_id, currency) pairs, and the entries of one transaction are contiguous in the stream. In a single pass with no re-reads, return the closing balance per (account_id, currency) and the transaction_id of every transaction whose entries do not sum to zero within each currency. State your time and space bounds.
Approach
- Normalise the sign at read time from
direction, not from the amount:signed = +amount_minorfor debit,-amount_minorfor credit (state which convention you picked). The schema constrainsamount_minor > 0precisely so the sign lives in exactly one place. - Hold one hash map keyed
(account_id, currency)to an int64 running total. Twenty million keys at 16 bytes of payload plus map overhead is order 1 GB in most runtimes — quote the number, and offer the fallback: partition the stream byhash(account_id) % Pand run P passes for 1/P of the memory. - Ride the zero-sum check on the same pass. Because a transaction's entries are contiguous, keep a tiny
currency -> int64map for the currenttransaction_idonly, test it against zero on the boundary and at EOF, then clear it. That is O(currencies in one transaction), typically one or two. - Bound the arithmetic explicitly. Int64 holds about 9.22e18, so overflowing one account across 500 million entries needs an average of 1.8e10 minor units per entry — safe here, but use a checked add so an adversarial file fails loudly rather than wrapping.
- Complexity: O(n) time, O(distinct account-currency pairs) space, one sequential pass, no sort. The zero-sum check adds no asymptotic cost, which is the argument for doing it here rather than in a second job.
Follow-up
- Entries of a transaction are no longer contiguous. What does the zero-sum check cost now, and which is cheaper: buffering open transactions or an external sort on
transaction_id? - How would you produce the same balances as of an arbitrary
business_datewithout a second full scan? - The job is restarted after a crash halfway through the file. What makes the second run produce identical output?
Compute peak held exposure from overlapping authorisation holds
An account has up to 2 million authorisations on one business date: (auth_id, amount_minor, created_at, expires_at) with the hold live over the half-open interval [created_at, expires_at), plus capture events (auth_id, captured_minor, captured_at) that reduce the hold at captured_at, and explicit reversals that drop the remainder to zero. Timestamps are microsecond-precision timestamptz. Return the maximum total held amount across the day and the earliest instant it is reached, with complexity. Then say what changes if the deliverable is the peak per minute instead.
Approach
- Expand each authorisation into signed delta events rather than reasoning about intervals:
+amountatcreated_at,-remainingatexpires_at,-captured_minorat eachcaptured_at,-remainingatreversed_at. The problem collapses to a running sum over a sorted event list. - Sort the 2n to 4n events by
(timestamp, sign)with negative deltas ordered first on a tie. The half-open convention forces that: at exactlyexpires_atthe hold is already gone, so a-must land before a+at the same instant or you report a one-microsecond peak that never existed. O(n log n) time, O(n) space. - Sweep once, tracking
running,bestandbest_at, taking the first instant that attains the maximum. Say out loud which tie rule you are using — 'the peak' is ambiguous when the same level is reached twice, and the caller needs to know which instant they are being handed. - If timestamps are bucketed (1,440 minute buckets for the per-minute variant), drop the sort for a difference array: add the delta at the start bucket, subtract at the end bucket, prefix-sum once. O(n + B) time and O(B) space, strictly better, at the cost of answering only at bucket resolution.
- Assert the invariant during the sweep:
runningmust never go negative. A negative total means a capture exceeded its authorisation, which is an invariant violation upstream rather than a sweep bug — fail loudly instead of clamping at zero and reporting a plausible number. - Handle carry-in: a hold created before the window contributes its remaining amount as the sweep's initial value, not as a
+event inside the window. Omitting that is the off-by-a-day that makes the first minute of every day look artificially quiet.
Follow-up
- An incremental authorisation raises an existing hold after the fact. Where does that event go, and does it disturb the tie rule?
- You now need the peak for 10 million accounts inside a nightly window. What changes, and what must the partitioning key be?
- The peak sizes a funding transfer. Does the business date or the timestamp decide which day that transfer lands on?
Rebuild a statement with a running balance from entries alone
From ledger_entry(entry_id bigint and monotonic, account_id, direction, amount_minor, currency, business_date, posted_at) produce one month's statement for a single account: every entry in order, its signed amount, and the running balance after it, starting from a supplied opening balance. The account is a customer deposit, so a credit increases it. Also return the first business_date in the month on which the running balance went negative, or null if it never did. Write the query using window functions, state which frame you depend on, and say what makes the ordering deterministic.
Approach
- Sign the amount first: CASE WHEN direction = 'credit' THEN amount_minor ELSE -amount_minor END, because the sign lives in direction and never in the column. State the convention out loud, since a customer deposit is a liability of the institution and a credit increases it, while for an asset account the same expression inverts.
- Compute the running balance as SUM(signed) OVER (PARTITION BY account_id ORDER BY business_date, entry_id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), then add the opening balance as a scalar. The opening figure is a constant for the statement, not a second window.
- Be explicit about the frame, because it is the whole exercise: with an ORDER BY and no frame clause the default is RANGE UNBOUNDED PRECEDING AND CURRENT ROW, which includes every peer row tied on the ordering key. Ordering on business_date alone therefore reports the same end-of-day figure on every line of that day, and the statement still looks plausible.
- Make the order deterministic with a column that has no ties: business_date has ties by construction, entry_id is monotonic and unique, and posted_at is neither guaranteed unique nor correct as an ordering key when a backdated entry posts late.
- Get the first breach from the same computation rather than a second pass: wrap the running balance in a subquery and take MIN(business_date) FILTER (WHERE running_balance_minor < 0). LAG is the wrong tool here, because the question is about a level rather than a change.
- Support it with an index on (account_id, business_date, entry_id) so the partition and the order both come from the index; then confirm in EXPLAIN that there is no Sort node above the index scan.
Worked solution 25 min
- Seed one account with 40 entries across a month, including three on the same business_date with mixed directions and one backdated entry posted late.
- Write the signed-amount expression and the SUM window with an explicit ROWS frame ordered by (business_date, entry_id).
- Wrap it and add MIN(business_date) FILTER (WHERE running_balance_minor < 0) as first_negative_date.
- Re-run with the frame clause removed and with entry_id dropped from the ORDER BY, and record exactly which rows change.
Follow-up
- An entry for 3 March posts on 7 March, after the statement for that week was sent. Where does it appear, and what does the running balance do?
- The same statement has to be reproducible a year from now. What stops it changing, and what would silently change it?
- At what account size does deriving this per request stop being viable, and what would you materialise first, a daily closing balance or a monthly one?
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.
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?
Can you debug a failing test case in a provided code snippet?
Can you debug a failing test case in a provided code snippet?
Approach
- Name the failure you are designing for, then the recovery path.
- Name the read and write paths separately; they rarely have the same bottleneck.
- State the consistency you need, and where you are willing to be stale.
Follow-up
- How does this behave when that dependency is down for an hour?
- What breaks first when traffic grows ten times?
Describe how you would troubleshoot a complex network fault or a perfo…
Describe how you would troubleshoot a complex network fault or a performance bottleneck.
Approach
- Name the failure you are designing for, then the recovery path.
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Choose a partition key and say what query it makes expensive.
Follow-up
- How does this behave when that dependency is down for an hour?
- What breaks first when traffic grows ten times?
How would you design a system for a medicine delivery service or a ban…
How would you design a system for a medicine delivery service or a banking transaction platform?
Approach
- Choose a partition key and say what query it makes expensive.
- State the consistency you need, and where you are willing to be stale.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Explain the principles of Microservices architecture and how you manag…
Explain the principles of Microservices architecture and how you manage service communication.
Approach
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
- 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?
What are the differences between First and Single in LINQ?
What are the differences between First and Single in LINQ?
Approach
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Paginate an append-only statement endpoint under continuous writes
Statement reads come from ledger_entry (entry_id bigint monotonic primary key, transaction_id uuid, account_id bigint, direction, amount_minor bigint, currency, source_type, source_id, business_date date, posted_at timestamptz). The caller is a merchant dashboard fetching one account newest-first, 100 rows per page, while new entries are still being appended. Offset pagination is in production and support reports rows appearing on two consecutive pages. Design the replacement: the index, the exact WHERE clause and ORDER BY, the cursor's contents and encoding, and what happens when a caller reuses a cursor an hour later.
Approach
- Name the mechanism of the duplication rather than blaming the sort. Each page is a separate statement with its own snapshot; under a descending order, every row inserted between page 1 and page 2 shifts the whole result one position later, so the last row of page 1 reappears as the first row of page 2. The cost is bad too: OFFSET n scans and discards n rows, so deep pages are O(offset + k).
- Replace it with a keyset predicate over the unique, monotonic ordering column: WHERE account_id = $1 AND entry_id < $cursor ORDER BY entry_id DESC LIMIT 100. That is one index descent plus 100 rows, O(log n + k), and the cost does not change with depth.
- Index (account_id, entry_id). A B-tree is scanned in either direction at the same cost, so a separate DESC index buys nothing. If the endpoint also filters on a date range, the tuple becomes (account_id, business_date, entry_id) and the predicate becomes a row-value comparison, (business_date, entry_id) < ($d, $e), which still resolves to a single descent.
- Make the cursor opaque and self-describing: base64 of the anchor tuple plus a hash of the filter set and an encoding version. A cursor presented with a different account or date range is rejected with a distinct code rather than quietly returning another account's rows.
- State the validity rule. Because the table is append-only, the anchor row is never deleted, so a keyset cursor stays valid indefinitely and an hour-old cursor simply resumes. Add one caveat for machine consumers: entry_id is allocated before commit, so an entry with a lower id can become visible after a reader has already passed it. A human statement page never notices, but a feed consumer needs a lag window or a commit-ordered marker rather than the newest id.
Worked solution 25 min
- Insert 500 entries for one account, fetch LIMIT 100 OFFSET 0 ordered by entry_id DESC, insert five more entries, then fetch LIMIT 100 OFFSET 100 and record which entry_ids appear in both results.
- Write the keyset query, repeat the identical interleaving, and confirm no entry that existed at page-1 time is returned twice or skipped.
- Run EXPLAIN (ANALYZE, BUFFERS) on both queries at offset 0 and at offset 100000 and compare the buffers read.
- Define the cursor as base64 of {entry_id, filter_hash, v} and implement the rejection branch for a filter_hash mismatch.
- Write down the allocation-versus-commit caveat and decide explicitly whether this endpoint is a human statement view (no mitigation needed) or a machine feed (needs a lag window).
Follow-up
- The dashboard now wants to sort by amount_minor descending. What breaks, and what does the cursor have to contain to fix it?
- A caller asks for a total page count. What does that cost on a 400-million-row table, and what do you offer instead?
- How would you support paging backwards from a cursor without re-sorting in the application?
Statement endpoint slows linearly with the entry count
A statement endpoint reads ledger_entry for one account and a date range and returns entry_id, amount_minor, currency, source_type and business_date, plus the display name of the counterparty account found through the other entry on the same transaction_id. p99 is 40 ms for a 20-entry month and 2.1 s for a 900-entry month. The database reports 901 statements per request, each under 1 ms, and no single query is slow. Diagnose the cause and give the fix, stating the statement count and the p99 you expect afterwards.
Approach
- Establish the shape before theorising. Divide the per-request statement count from pg_stat_statements by the rows returned: 901 statements for 900 rows is one driver query plus one per row, and because each is sub-millisecond it rules out a bad plan. The latency is round trips, not work.
- Identify the per-row statement by its normalised text. It will be a single-row lookup on ledger_entry joined to account, keyed by transaction_id, issued from the serialisation layer rather than the repository. Confirm it is lazy loading by checking that it disappears when the counterparty name is dropped from the response.
- Do the arithmetic as a division from the incident rather than a multiplication from assumptions. The 900-entry request issues 880 more statements than the 20-entry one, 901 against 21, for 2.06 s more wall time, so each extra statement costs at most about 2.3 ms end to end, and that is an upper bound because the larger response also carries more payload. Then measure one round trip on this path instead of assuming it: pool checkout, network, parse, bind, execute and per-row hydration in the driver, not network alone. A naive 1 ms network plus the 0.2 ms the database reports would cover only about 1.05 s, half the excess, so if a measured round trip really is that cheap then the N+1 is not the whole story and you owe an explanation for the rest before proposing a fix.
- Replace the per-row lookup with one batch statement: collect the transaction_ids from the driver query and fetch all counterparty entries with WHERE transaction_id = ANY($1), then build the map in memory. Two statements per request, independent of the range width.
- Check the index before declaring victory. The batch query needs an index on ledger_entry(transaction_id); without it ANY($1) degrades to a sequential scan over the entry table and the fix becomes a worse regression than the bug.
Follow-up
- What changes if the statement must paginate at 200 entries per page?
- A transaction can carry more than two entries once fees have their own leg. What does your query return then, and what should the counterparty column mean?
- What would have caught this before production, given that no individual query is slow?
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Practice prompt ↗Practice prompt ↗Practice prompt ↗Worked solution ↗02References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Practice prompt ↗Practice prompt ↗03Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Practice prompt ↗Practice prompt ↗04Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Practice prompt ↗Practice prompt ↗06Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Practice prompt ↗Practice prompt ↗07Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Nobody is scoring your stamina at three in the morning. What carries weight is which signal told you something was wrong, what you measured before touching anything, what you rolled back versus what you fixed forward, and why you picked one. 'We restarted it and it went away' is a story about not knowing.
Which of our core values resonates with you most, and why?
Which of our core values resonates with you most, and why?
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?
- What did you decide not to do, and why?
Tell us about a time you reviewed a design that didn't feel right; wha…
Tell us about a time you reviewed a design that didn't feel right; what did you do?
Approach
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Describe a time you had a conflict with a colleague and how you resolv…
Describe a time you had a conflict with a colleague and how you resolved it.
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
- 01
Which of our core values resonates with you most, and why?
- 02
Tell us about a time you reviewed a design that didn't feel right; what did you do?
- 03
Describe a time you had a conflict with a colleague and how you resolved it.
Is this an official Lloyds Banking Group interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Lloyds Banking Group. Rounds and questions reflect what candidates have reported, not a process Lloyds Banking Group has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long does the interview process typically take?
The process can range from a few weeks to several months depending on the role and team. It is common to have gaps between stages, so plan your job search accordingly.
PracHub interview research ↗What differentiates successful candidates?
Successful candidates are those who can balance technical expertise with a clear understanding of the bank’s values. They are not just coders; they are engineers who care about security, scalability, and the impact of their work on the customer.
PracHub interview research ↗Is there remote or hybrid work?
Lloyds Banking Group generally operates on a hybrid model. Specific expectations regarding office presence will be discussed during your initial screening or hiring manager interview.
PracHub interview research ↗How should I prepare for the "values" questions?
Do not memorize the values. Instead, reflect on your past experiences where you demonstrated trust, boldness, or customer-first thinking. Be ready to tell these stories using the STAR+R framework.
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