Interview guides · CARD91
CARD91 Software Engineer Interview Guide
CARD91 Software Engineer interview guide: reported Java, OOP, DSA and SQL questions, answer approaches and a 7-day plan for the backend payments role.
The 60-second brief
CARD91 builds payment infrastructure: candidates describe a payment gateway and related financial services, with backend systems that process transactions and connect to banking partners. The Software Engineer role is a backend position. Reported day-to-day work is writing services in Java or a similar object-oriented language, turning product requirements into technical specifications with product managers, tuning database queries and schemas for peak transaction volume, reviewing teammates' code, and tracing production problems to a root cause with a lasting fix. Reported challenges include many requests arriving at once, keeping financial data correct, and integrating with several banks.
This guide is for candidates interviewing for the Software Engineer role at CARD91.
No named rounds are reported for CARD91. Candidates describe a move from a conversation about your background to technical deep-dives, over a process that often spans a week or two depending on scheduling.
Focus areas
- Java
- Data Structures & Algorithms (DSA)
- Object-Oriented Programming (OOP)
Overview
The reported work is backend engineering for a payment gateway: Java or a similar object-oriented language, relational databases that must stay fast at peak transaction volume, and production issues that need a root cause rather than a patch. The challenges candidates list are concurrency, data integrity and integration with banking partners. That shapes how to practise: when you explain a Java concept, attach it to a money-moving example, such as a payment method interface with card and bank-transfer implementations, or a refund that must not run twice.
The 4 questions candidates report are all language and design fundamentals: the difference between interfaces and abstract classes, the OOP concepts and how you apply them day to day, handling exceptions in a production application, and == versus .equals in Java. None needs a clever trick. Each is answered well by a precise rule, a short code example, and a case where choosing wrongly causes a real bug. For == versus .equals, that means string literals versus new String, the cache behind boxed Integer values, and the equals and hashCode contract in hash-based collections.
Beyond those questions, candidates report time and space complexity analysis, sorting and searching, code optimization and implementing a data structure from scratch. They also report SQL, with a specific warning that candidates who cope with coding questions can stumble on complex query optimization. Project experience is described as a major discussion point, so prepare the architecture of two or three past projects and the reasons behind your decisions. No round names or order are reported, so confirm the format with your recruiter and prepare for both spoken fundamentals and written code.
Where to focus your preparation
CARD91 candidates report no named rounds, only a general progression from a background conversation to technical deep-dives, so this guide treats the loop as one preparation map and not a fixed sequence.
Preparation focus
Candidates report no round names or order for CARD91. What they do describe is a process that starts with an assessment of your background and moves into detailed technical conversations, often finishing within a week or two depending on scheduling. The topics reported are Java and OOP fundamentals, DSA problem solving with time and space complexity, sorting and searching, building a data structure from scratch, SQL and query optimization, and a close look at your own projects. Because the format is unconfirmed, ask your recruiter which stages exist, whether you will write code live, and in which language, then prepare for spoken explanations and written code alike.
What to demonstrate
- Core Java and object-oriented fundamentals, as in the reported questions on interfaces, abstract classes, OOP concepts, exceptions and == versus .equals
- Time and space complexity analysis of the solutions you write, plus sorting, searching and optimization
- SQL and relational design, including how you would speed up a slow query
- How you describe the architecture of your past projects and the reasoning behind your choices
How to prepare
- Write a short Java example for each reported fundamentals question and rehearse the spoken version, ending each with a bug caused by the wrong choice
- Implement a hash map with chaining and a binary heap from scratch in Java, and state the complexity of every operation aloud
- Practise reading an EXPLAIN plan for a transactions-by-account-and-date query and fix it with an index in the right column order
- Write a one-page architecture note for two past projects covering the decisions, the alternatives you rejected, and one production failure you diagnosed
Where candidates lose points
Answering the interface, abstract class and OOP questions as vocabulary lists
Lead with a decision rule: an interface defines a contract that unrelated classes can implement and a class can implement many; an abstract class holds shared state, constructors and partial implementation for a family of types. Then show one example, such as a payment method interface with card and bank-transfer classes. For OOP, give one line of your own code for each of encapsulation, inheritance, polymorphism and abstraction.
Treating == and .equals as interchangeable, or explaining == only with strings
State that == compares references for objects and values for primitives, while .equals compares logical equality when the class overrides it. Show why two string literals can compare equal with == through the string pool while new String objects do not, and why boxed Integers compare equal only inside the small cached range. Mention that overriding equals requires a consistent hashCode for HashMap and HashSet, and that Objects.equals handles null.
Describing exception handling as try-catch plus logging
Prepare the full answer: catch where you can act, wrap with the cause, close resources with try-with-resources, separate retryable from non-retryable failures, protect retried writes with idempotency, roll back transactions, and log once with a correlation id and no sensitive data. Back it with one incident from your own work.
Preparing coding and DSA but neglecting SQL query optimization
Candidates report that SQL tuning is where otherwise strong coders struggle. Practise reading an EXPLAIN plan, choosing composite index column order for a filter-plus-sort query, spotting a function call that blocks index use, and explaining why a query slows down only at peak volume.
Walking through what a past project does without explaining why it was built that way
Candidates report that project architecture and the reasons behind decisions are a main discussion point. For each project, prepare the alternatives you rejected, the trade-off you accepted, one production failure you diagnosed and its root cause, and one number you can defend. Open with the problem, not the tech list.
Questions & practice
Can you explain the primary differences between interfaces and abstract classes in Java?
Medium · Technical Fundamentals and Language Prof
Approach
- Start with the structural differences. An abstract class can hold instance fields, constructors and members at any access level, and a class can extend only one of them. An interface has no instance state (its fields are implicitly public static final constants) and no constructors, and a class can implement any number of interfaces.
- Bring the comparison up to modern Java. Since Java 8 interfaces can carry default and static methods, and since Java 9 private helper methods. So 'interfaces have no method bodies' is out of date. The real dividing line is state and constructors, plus single versus multiple inheritance.
- Give the design rule with one example each. Use an interface for a capability that unrelated types share (Comparable, Runnable, any functional interface used with lambdas). Use an abstract class when closely related types share state and a fixed skeleton, as in the template method pattern, where the base class owns the algorithm and subclasses fill in the abstract steps.
- Cover API evolution. Adding an abstract method to a published interface breaks every implementor at compile time. A default method avoids that. Adding a concrete method to an abstract class leaves existing subclasses compiling.
- Name the resolution rules for conflicts. A method inherited from a class wins over an interface default. If two interfaces supply the same default method, the implementing class must override it and can call a chosen one with InterfaceName.super.method().
Follow-up
- A class implements two interfaces that both define the same default method. What does the compiler do, and how do you resolve it?
- Why can an abstract class have a constructor when it can never be instantiated directly?
- When would you combine the two, with an interface as the contract and an abstract base class as a partial implementation, and what does that buy you?
What are the OOPS concepts, and how do you implement them in your daily coding?
Medium · Technical Fundamentals and Language Prof
Approach
- Name the four pillars and give each a concrete Java mechanism, not just a definition. Encapsulation is private fields behind methods that enforce invariants. Abstraction is depending on an interface rather than a concrete class. Inheritance is extends and implements. Polymorphism is a call that dispatches on the runtime type.
- Make encapsulation real with an example from your own code: a class whose constructor validates input and whose fields are final, with defensive copies of mutable collections going in and out, so no caller can put the object into an invalid state.
- Separate the two kinds of polymorphism. Overloading is resolved at compile time from the static argument types. Overriding is resolved at runtime by dynamic dispatch. Static methods are hidden, not overridden, so they never dispatch on the runtime type.
- Show judgment on inheritance. Prefer composition when the relationship is 'has-a' or the behaviour varies independently. Respect Liskov substitution: a subclass must not tighten preconditions or throw where the parent would not. Replace long instanceof or switch chains with a strategy interface and one implementation per case.
- Tie the pillars to daily practice: constructor-injected interfaces so a class can be unit tested with a fake, small classes with one reason to change, and records or immutable value classes for data that should not change after creation.
Follow-up
- Given overloaded methods m(Object) and m(String), which one runs for m(x) when x is declared as Object but holds a String, and why?
- Show a case where inheritance broke something and how composition would have avoided it.
- How do the SOLID principles relate to these four concepts, and which one do you apply most often?
Can you describe the difference between == and .equals in Java?
Medium · Technical Fundamentals and Language Prof
Approach
- State the core rule. On primitives, == compares values. On references, it compares identity: whether both variables point to the same object. equals() is a method inherited from Object whose default implementation is also identity, and classes such as String, Integer, BigDecimal, List and records override it to compare values.
- Give the classic traps. String literals are interned, so "abc" == "abc" is true, but new String("abc") == "abc" is false, and so is a string built at runtime by concatenation. Autoboxing through Integer.valueOf caches -128 to 127 by default, so two boxed 127s are == while two boxed 128s usually are not. Comparing a null Integer with == against an int unboxes it and throws NullPointerException.
- Cover null safety. a.equals(b) throws when a is null. Write "literal".equals(x) or Objects.equals(a, b) instead. For enums, == is correct and null-safe, because each constant is a singleton.
- Explain the equals contract and its hashCode partner. equals must be reflexive, symmetric, transitive and consistent, and x.equals(null) must return false. Equal objects must return the same hashCode, or HashMap and HashSet lookups silently miss.
- Raise the money-related edge case. BigDecimal.equals compares scale as well as value, so 1.0 and 1.00 are not equal, while compareTo returns 0. Compare amounts with compareTo, or normalise the scale first. Never use double for currency.
Follow-up
- What breaks if a class overrides equals but not hashCode and you put its instances in a HashSet?
- In an equals implementation, would you use getClass() or instanceof, and how does that choice affect symmetry when subclasses exist?
- Why does new BigDecimal("2.50").equals(new BigDecimal("2.5")) return false, and how would you compare two amounts correctly?
Hold a per-tenant active cap against concurrent creates
Hard · write skew · isolation levels · constraints
A tenant on the standard plan may hold at most 50 resources with status='active'. The create handler runs SELECT count(*) FROM resource WHERE tenant_id = $1 AND status = 'active', compares to 50, then inserts. Two creates arrive 3 ms apart on different instances and the tenant lands at 51. Name the anomaly, say whether PostgreSQL 16 READ COMMITTED or REPEATABLE READ prevents it and why, then give an implementation that holds the cap at READ COMMITTED with the exact statements. Finally, say what changes when the cap is 'at most one running export per tenant' on job_run.
Approach
- Name it: write skew. The two transactions read an overlapping set and write disjoint rows, so there is no row-level conflict for the engine to detect and each commit is individually legal.
- Rule out the levels precisely. READ COMMITTED takes a fresh snapshot per statement and takes no lock on the counted rows, so both see 49. PostgreSQL's REPEATABLE READ is snapshot isolation: it removes non-repeatable reads and phantoms within the snapshot but still admits write skew, because the anomaly is not a re-read of a changed row, it is a read of a set that a concurrent transaction invalidates. Only SERIALIZABLE closes it, by tracking the read dependency and aborting one transaction with SQLSTATE 40001 — a guarantee that exists only if the application re-runs the whole transaction from the read.
- Convert the set predicate into a single-row conflict: keep tenant.active_resource_count and run UPDATE tenant SET active_resource_count = active_resource_count + 1 WHERE tenant_id = $1 AND active_resource_count < 50 in the same transaction as the INSERT. Zero affected rows is the cap, returned as 409. The row lock serialises the decision at any isolation level, and contention is bounded to one tenant's row — which is also the fair-scheduling unit, unlike a global counter that would convoy every tenant behind one row.
- State the cost you just took on: a counter is a second source of truth that can drift, so every path that changes status must adjust it inside the same transaction, and a periodic reconciliation has to exist, with resource_revision as the authority for what the count should have been.
Follow-up
- A resource moves from archived back to active. Which statements change, and what breaks if the counter update and the status change land in different transactions?
- The cap becomes plan-dependent and a plan can change mid-month. Where does the number 50 live, and who reads it?
Write the update path that detects a concurrent edit
Medium · optimistic concurrency · transactional outbox · audit history
resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.
Approach
- One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
- Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
- State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
- Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
Follow-up
- A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
- Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
Evolve the resource contract without breaking integrations you cannot upgrade
Hard · versioning · backward compatibility · deprecation
GET /v1/resources/{id} returns status from a four-value enum ('draft','active','archived','deleted'), a numeric version, and the body inline. Consumers are a browser app you deploy and roughly 300 server-side integrations, some untouched for two years, that switch exhaustively on status and parse ids as JSON numbers. You must add a 'pending_review' status, move bodies over 256 KB to a body_ref pointer, and expose per-field change history from resource_revision. Specify the compatibility policy, the wire changes, how both generations are served, and the evidence that lets you remove the old shape.
Approach
- Write the policy first and date it: fields are added, never retyped or repurposed; consumers ignore unknown fields; an unknown enum value maps to a documented fallback; nothing is removed until telemetry shows no caller reads it. Then say the uncomfortable part out loud - v1 shipped without the unknown-value rule, so 300 running integrations have no fallback, and no server change can install one into code that is already deployed.
- That single fact forces per-request negotiation rather than a server-side default. Keep one internal model and select a serialiser from an explicit version in the request, and default a caller that sends nothing to the oldest supported version. Defaulting to the newest is the change that breaks every integration that never asked for anything, on the day you ship.
- Downgrade 'pending_review' for old callers to the nearest state they already handle, 'draft', and state the loss explicitly: those integrations cannot see review state and will treat the resource as editable. If that is unacceptable for one integration, the remedy is moving it to the new version, not a cleverer projection - there is no mapping that invents a state the client has no code for.
- Make the body change additive. Old callers keep
bodyinline; the new shape addsbody_refand a size field, and resources over the limit are served to old callers by resolving the pointer server-side or by refusing with a documented code, chosen once and published. Never repurposebodyto carry the pointer: a client that renders it shows a storage key to a user, and that failure is silent, where a missing field would have been loud. While you are here, serialise BIGINT ids as strings in the new shape - a browser parsing JSON numbers gets IEEE-754 doubles, exact only to 2^53 - and treat that as its own breaking change requiring the same negotiation, not a quiet fix.
Follow-up
- An old integration submits a status transition while the resource is really in 'pending_review'. What does the write path accept, and what does it reject?
- Two years on you want to delete the v1 serialiser. What evidence makes that safe, and who must be contacted before it happens?
Make the resource write endpoint safe for retrying clients
Easy · idempotency · unique constraints · retries
The edge API accepts POST /v1/resources at 1.2k writes/second peak under a 400 ms p99 budget, and clients retry on timeout. idempotency_key has PRIMARY KEY (tenant_id, idempotency_key) plus request_fingerprint CHAR(64), state in ('in_flight','succeeded','failed'), response_status, response_body, resource_id, locked_until and expires_at. Specify the exact sequence the handler runs: what the second request does when it arrives while the first is still executing, what a retry carrying a different body receives, what happens when a process dies mid-request, and how the table is kept from growing without bound.
Approach
- Start from the race rather than the happy path. Insert the key row first, in its own short transaction, with state 'in_flight' and locked_until set to now() plus the request deadline. The unique constraint on (tenant_id, idempotency_key) is the arbitration mechanism: exactly one caller commits the insert, and every other caller takes a unique violation (SQLSTATE 23505) and becomes a follower. Reading the table and then inserting is a check-then-act race that both callers pass, and they pass it most often under the load that generates the retries.
- Give the follower a branch for every state it can observe, with no fall-through to doing the work: fingerprint mismatch means the key is being reused for a different request, so reject with 422 and never replay the stored response; 'succeeded' replays response_status and response_body verbatim; 'failed' allows a fresh attempt; 'in_flight' with locked_until in the future is answered with 409 and a retry hint, or a short bounded wait, because the only alternative is executing the effect twice.
- Commit the effect and the record of the effect together. The resource insert, its resource_revision row, its outbox_event row and the transition of the key row to 'succeeded' all happen in one transaction, so no crash can leave the work done and the key still 'in_flight'. The earlier insert is deliberately a separate transaction: it has to be visible to a concurrent caller before the work starts, which an uncommitted row is not.
- Close the wedged-key path. A process that dies after the insert leaves a row that would otherwise block that key forever, which is what locked_until exists for. Reclaim with a single conditional statement, UPDATE ... SET state='in_flight', locked_until=now()+interval WHERE ... AND state='in_flight' AND locked_until < now(), and treat a zero rowcount as losing the reclaim. Two statements that read and then update reintroduce the original race one layer down.
Follow-up
- The stored response body averages 200 KB and this table is now the largest in the database. What do you store instead, and what does a replay return once the body has been pruned?
- A client library generates a fresh idempotency key on every attempt. What breaks, which layer should have caught it, and does the server have any defence?
p99 jumped on one listing filter while p50 stayed flat
Medium · query planning · composite index · percentiles
After a release that added an owner_user_id filter to the resource listing, p99 rose from 90 ms to 1.9 s while p50 stayed at 40 ms. Traffic and row counts are unchanged. resource carries the index (tenant_id, status, updated_at DESC, resource_id DESC). The new query filters tenant_id and owner_user_id, orders by updated_at DESC, resource_id DESC, and takes 20 rows. On PostgreSQL, explain the shape of the regression, prove it from a query plan, and give the index you would add.
Approach
- Start from the shape. A flat p50 with a moved p99 means a subset of requests changed cost, not all of them, so the first job is naming the subset. Bucket the endpoint's latency by the tenant's row count; the natural hypothesis is that large tenants are a small share of requests and all of the tail.
- Get the plan for the new query on a large tenant with EXPLAIN (ANALYZE, BUFFERS). Expect an index scan over the tenant's range, a filter discarding most of it, then a Sort feeding the Limit, possibly reporting Sort Method: external merge Disk. Read actual rows on the scan node, not estimated.
- Explain why the existing index cannot serve it. A composite B-tree is seekable only as a left prefix, and with no equality predicate on status the scan cannot treat updated_at as an ordering, because rows in the tenant's range are ordered by status first. Everything matching must be read and sorted before LIMIT 20 can apply, so a tenant with 400,000 rows pays 400,000 rows to return 20.
- Add (tenant_id, owner_user_id, updated_at DESC, resource_id DESC). Equality on the first two columns leaves the index ordered by updated_at within that pair, so the plan becomes an index scan that stops after 20 rows with no Sort node. PostgreSQL can scan a B-tree backwards, so the DESC markers matter only if the two sort columns ever disagree in direction; keeping them explicit documents the order the keyset cursor depends on.
Follow-up
- The endpoint paginates with OFFSET. What does page 500 cost with your index, and what does the keyset version cost?
- How would you have caught this before release, given that a 10,000-row seed database produces the same plan shape at an unnoticeable cost?
Suggested practice checklist
Day 1 — Confirm the format and pick your project stories
- Write the questions to send your recruiter: which stages exist, whether you will write code live, which language, and whether SQL is tested.
- Choose two past projects and write a one-page architecture note for each: the problem, the components, three decisions and the alternative you rejected for each.
Deliverable: A written list of recruiter questions and two one-page project architecture notes.
Day 2 — Interfaces, abstract classes and == versus .equals
- Write a Java example with a payment method interface, an abstract base class holding shared state, and two concrete classes, then explain when you would pick each.
- Write a small program that shows string literal versus new String comparison, boxed Integer comparison inside and outside the cache range, and a class whose equals is overridden without hashCode failing in a HashSet.
- Answer both questions aloud without notes, ending each answer with a bug caused by the wrong choice.
Deliverable: One runnable Java file with both demonstrations and a spoken answer to each of the two questions.
Day 3 — OOP concepts and exception handling
- Take the day 2 payment classes and annotate where encapsulation, inheritance, polymorphism and abstraction each appear, so your OOP answer uses your own code.
- Write a custom exception hierarchy for a payment flow with one checked and one unchecked type, wrap a lower-level exception with its cause, and add try-with-resources around a database connection.
- Write a STAR story about a real production error you handled, naming the decision you made and the measured result.
Deliverable: An annotated class file, an exception-handling example, and a written STAR story.
Day 4 — DSA and complexity from scratch
- Implement a hash map with chaining and resizing, and a binary min-heap, in Java, and write the time and space complexity of every operation.
- Write a comparison of merge sort, quicksort and heap sort covering worst case, memory, stability and behaviour on nearly sorted input, then explain how you would search a very large sorted dataset.
- Take one of your working solutions and refactor it for readability, then say what you changed for performance and why.
Deliverable: Two working data structures, a sorting comparison table, and a before-and-after refactor with the complexity stated.
Day 5 — SQL, concurrency and slow queries
- Work the practice question on writing an update path that detects a concurrent edit, and say what the caller should receive when the version check fails.
- Design an index for a transactions-by-account-and-date query, read its EXPLAIN plan before and after, and explain the column order.
Deliverable: Two solved SQL practice questions and an index design with before-and-after plans.
Day 6 — Retries, production debugging and review disagreements
- Work the practice question on a latency jump that affects only the 99th percentile on one listing filter, writing down your debugging sequence.
- Work the practice question on turning a code review disagreement into a decision, and rehearse it as a STAR story.
Deliverable: A retry-safe write design, a written debugging sequence, and a rehearsed review-disagreement story.
Day 7 — Full mock
- Answer all 4 reported fundamentals questions aloud in one sitting, then explain one of your project notes from day 1.
- Work the practice question on reversing your own decision, and compare its story shape with your project notes.
- Write 2 or 3 questions to ask the team about its current technical challenges and its stack.
Deliverable: A completed spoken mock covering the 4 reported questions, one rehearsed reversal story, and a written list of 2 or 3 questions to ask.
Project stories and production-error answers
No separate behavioral round is reported for CARD91. Candidates do report that project experience is a major discussion point, with attention to architecture and the reasoning behind technical choices, and the reported exception-handling question invites a story as well as a rule. For a backend payments role, prepare stories about production failures, correctness under load, and working with colleagues through code review. Structure each with Situation, Task, Action and Result.
- Walk me through the architecture of a project you built and the reasons behind its main technical decisions.
- Tell me about a production issue you traced to a root cause: how you found it and what long-term fix you made.
- Describe a code review where you disagreed with a colleague and how the disagreement was resolved.
- Tell me about a time you explained a technical concept or a design decision to someone who is not an engineer.
How do you handle exceptions effectively in a production-grade application?
Medium · Technical Fundamentals and Language Prof
Approach
- Open with a rule, then a case. Rule: catch an exception only where you can do something useful (retry, compensate, translate, or return a meaningful error); otherwise let it propagate. Never leave an empty catch block, and avoid catching Exception or Throwable broadly, because that hides programming errors and out-of-memory conditions alongside the failure you meant to handle.
- Cover Java mechanics concretely. Use try-with-resources for connections, statements and streams so they close even on failure. Choose checked exceptions for recoverable conditions the caller must handle and unchecked ones for programming errors. When you wrap an exception at a layer boundary, pass the original as the cause so the stack trace survives. Do not use exceptions for ordinary control flow.
- Make it production-specific. Separate retryable failures (a timeout or a 503 from a bank connection) from non-retryable ones (validation errors, a declined payment). Retry only operations that are idempotent, or protect them with an idempotency key, using exponential backoff with jitter and a retry cap. Roll back the database transaction on failure so no half-finished money movement is left behind.
- Cover visibility. Log once, at the boundary that handles the error, with a correlation or request id and no card numbers or personal data in the message. Map internal exceptions to stable error codes at the API edge, and emit a metric so alerting fires on rates, not on a single log line.
- Because this question sits among the behavioral ones, finish with a short STAR story from your own work: a real incident where an error was swallowed or mishandled, what you changed, and the measured result. The mistake that sinks it is a textbook list with no real case. Use "I" throughout, name your own decision, and say what you would do differently.
Follow-up
- What is the difference between a checked and an unchecked exception, and which would you use for a declined payment?
- A downstream bank call times out after you have sent the request. How do you avoid charging twice when you retry?
- How do you decide what to log when an exception is caught, and how do you stop the same error being logged at every layer?
Turn a code review disagreement into a decision
Easy · code review · optimistic concurrency · communication
A colleague's change updates a row with UPDATE resource SET version = version + 1 WHERE resource_id = $1 AND version = $2 and treats an affected-row count of zero as a successful no-op. You read that as a silently lost update; they think returning 200 is friendlier to clients than returning a conflict. Describe how you have handled a review disagreement of this shape: what goes in the comment, when you leave the thread, and who decides. Then write the comment you would leave here, in under 80 words.
Approach
- Sort the disagreement before writing anything. A silently discarded write is a correctness claim about data; the choice between 409 and 412 is taste. Only the first justifies blocking a merge, and saying which one you are doing is most of the value of the comment.
- Make the claim reproducible in the comment itself with an interleaving rather than a principle: A reads version 7, B reads version 7, B commits version 8, A's predicate matches zero rows, A is told it succeeded and A's edit is gone.
- Offer the alternative with its cost attached: return 409 carrying the current version and the revision that won, so the client can re-read and re-apply. Note that automatic retry is not the fix, because a retry re-reads the winner's state and reapplies an intent formed against data that no longer exists.
- Apply an escalation rule you can state: two round trips on the thread, then a call, and the service's owner decides rather than the reviewer. A reviewer who cannot be overruled is a bottleneck with extra steps.
Follow-up
- Where would you put the test that fails if someone reintroduces the swallowed zero rowcount?
- The author says clients cannot handle a 409. How do you check whether that is true?
Reverse your own decision and price the reversal
Medium · reversibility · measurement · migrations
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
Approach
- State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
- Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
- Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
- Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.
Follow-up
- What in that decision was irreversible, and did you know it was irreversible when you made it?
- How did you tell the people who had already built on top of the original decision?