As a Software Engineer at Kroger, you are at the intersection of large-scale retail operations and cutting-edge digital transformation. You are not just writing code; you are building the infrastructure that powers one of the largest grocery retailers in the United States. Your work affects how millions of customers experience shopping, whether through Kroger's e-commerce platforms, supply chain logistics, or in-store digital tools.
The role demands a balance of technical rigor and a deep understanding of the retail ecosystem. You will work within complex environments involving Java, Go, Azure, and Kafka, contributing to a tech stack that must be both resilient and highly scalable. Whether you are optimizing backend microservices or improving the performance of customer-facing applications, your contributions ensure that Kroger remains a leader in a rapidly evolving digital marketplace.
While the interview process for some roles at Kroger can be quite straightforward, candidates report that Software Engineering interviews weigh both technical competency and alignment with Kroger's customer-first culture.
Recruiter Screen
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
reportedInput bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Interviews with Managers
reportedBecause the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
PracHub editorial advice for the preparation topics above.
Holding inventory reservations in a cache with a TTL while the authoritative count lives in the database
This creates two sources of truth that disagree under exactly the conditions you built the cache for. Redis replication is asynchronous, so a failover can lose writes that were already acknowledged to the client, and those lost writes are reservations customers are in the middle of paying for. Independently, a TTL that expires mid-checkout releases units to another buyer while the first customer's authorization is in flight, producing an oversell that no component logged as an error. Use the cache as a fast negative filter only — it may say 'definitely none left', never 'yes, it is yours' — and let the binding decision be a conditional write in the same transactional store as the reservation rows, so that losing the race returns zero affected rows instead of silent divergence.
Treating a payment timeout as a failure and retrying with a fresh idempotency key
An HTTP timeout carries no information about whether the provider processed the request; the call may have succeeded and the response been lost in either direction. Retrying with a new key creates a second authorization or a second capture, and the customer sees two pending amounts on their statement — which is also the version of this bug that reaches support rather than a dashboard. The correct shape is to persist the intent with its key before the call, retry with the same key, and if the operation is one the provider does not make idempotent, resolve it by querying for your own reference before issuing anything else. The structural requirement is that the outcome type has three values, not two: a boolean return type makes the bug unavoidable no matter how careful the caller is.
Starting work without saying what you are about to spend time on
State the plan before executing it: the approach, roughly how long it will take, and what you intend to leave hand-waved. That gives the interviewer a chance to redirect you in ten seconds rather than watching you spend fifteen minutes on the wrong sub-problem.
Treating a network call as though it were a local function call
A remote call can be slow, fail, or return after you stopped waiting, so name the timeout, the retry policy, and what the caller sees while the dependency is down. A call with no timeout turns one slow dependency into an exhausted thread or connection pool in every service upstream of it.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Fold a partitioned catalog event log into current variant state
Seven days of catalog events — about 800 million records of (variant_id, version, payload), payloads averaging 2 KB — sit across 32 partition files. Version is monotonic per variant, but there is no ordering across partitions. There are two million distinct variants. In a container with 512 MB of memory, produce the winning event per variant, then compare against an index snapshot of (variant_id, indexed_version) to emit the stale, orphaned and missing sets. State your passes, your I/O in bytes, the device latency and bandwidth your plan assumes, and your tie rule.
Approach
- Compute the bytes and the key count separately, because they point at different algorithms. 800 million records at a 2 KB payload plus a 64-byte header is about 1.7 TB on disk — the headers alone account for 51 GB — while there are only two million distinct variants. Memory is governed by the key count and I/O by the byte count, so an external merge sort of 1.7 TB is the expensive wrong answer, and explaining why is the substance of the question.
- Pass one folds to offsets rather than payloads. Hold
variant_id -> (version, partition_id, offset)at about 32 bytes of payload per entry plus map overhead, order 150 MB for two million keys, which fits in 512 MB. Retaining the 2 KB payloads instead would be about 4 GB, which does not. - Price pass one in bytes, not in records, and say which layout you are assuming. If the log is row-oriented with payloads inline, pass one streams the whole 1.7 TB — about 3,400 s at 500 MB/s — because seeking past each payload would be 800 million seeks and is strictly worse than reading them. If headers live apart from payloads (columnar segments, or a sidecar index of
(variant_id, version, offset)), pass one touches only 51 GB, about 100 s. Quote the layout before quoting the runtime. - Pass two fetches the winners, and selectivity decides the access pattern. Two million winners out of 800 million records is one in 400, an average stride of about 845 KB. One random read costs 0.1 ms; scanning 845 KB at 500 MB/s costs 1.7 ms. So seek: two million reads is about 200 s and moves 4.2 GB, against about 3,400 s and another 1.7 TB for a second full scan — roughly a 17x win, and about 3,600 s end to end rather than 6,800 s. The breakeven stride is latency times bandwidth, 0.1 ms * 500 MB/s = 50 KB, or one winner per 24 records; denser than that and the re-scan wins. On a 10 ms-seek device breakeven is 5 MB and two million seeks would cost 20,000 s, so the answer inverts — quote the device, not the rule of thumb.
- Sort the winning offsets by
(partition_id, offset)even though you are seeking: one file handle open at a time, every seek forward, readahead coalescing winners that land in the same block, at O(K log K) on two million entries. Keep 32 to 64 reads in flight so the 200 s is latency you overlap rather than latency you serialise. - Make the tie rule total and loud: order by
(version, partition_id, offset)and count ties on(variant_id, version)separately. A tie means a producer emitted two events at one version, which is an upstream defect; resolving it silently is how that defect survives a year unnoticed. - Reconcile against the index snapshot by merge or hash probe, emitting three disjoint sets —
indexed_versionbelow the winning version is stale, present in the index but not sellable under the folded state is orphaned, sellable but absent is missing. Sellability is a conjunction over the folded state (status = 'active', dimensions present whenis_shippable, a price in every active currency, a category assigned), never over an intermediate event. - Emit repairs as absolute assignments naming the target version, so the indexer's existing rule of discarding any event at or below the version it already holds makes a replayed repair a no-op. Complexity: one sequential pass over n records, O(K log K) to order the winning offsets, then K random reads — the log is read end to end once, not twice — and O(K) memory for K distinct variants.
Follow-up
- A partition file is truncated mid-record. What does the fold do, and what must it not do?
- The repair job and the live indexer both write while the fold runs. What makes the outcome decidable?
- Distinct variants grow to 200 million. Which of your two passes changes first, and into what?
Compute peak attempt rate per variant over a sliding window
Given one day of reservation attempts as (timestamp_ms, variant_id) — up to 300 million events across two million variants, arriving in timestamp order — compute for each variant the maximum number of attempts falling inside any 60-second sliding window, plus the start of the earliest window achieving it. The window is continuous, not a tumbling bucket. Give an algorithm linear in the number of events after grouping, with bounded memory per variant, and say precisely what that memory is bounded by.
Approach
- Group by
variant_id, then run two pointers over each group's timestamps: advance the right pointer one event at a time and advance the left whilet[right] - t[left] >= 60000, with the count beingright - left + 1. Neither pointer moves backwards, so each group costs O(n_v) and the whole job is O(n) after grouping — the partition or sort is the only superlinear step in the pipeline. - Justify examining only n windows: the maximum can always be attained by a window whose left edge sits on an event, because sliding a window without crossing an event cannot change its count. That is the step candidates skip and then cannot defend when asked why n windows suffice over continuous time.
- For the streaming form, hold a deque of timestamps per active variant and pop from the front while the front is older than 60 seconds. Memory per variant is bounded by the peak window count rather than by the day's event count — a variant at 1,000 attempts per second holds at most 60,000 timestamps, about 480 KB at 8 bytes each, and that is the number to quote when asked whether the job fits.
- Say why tumbling buckets are not a substitute, with the concrete case: 100 attempts at t = 59.9s and 100 at t = 60.1s give a tumbling maximum of 100 against a true sliding maximum of 200. The understatement can reach a factor of two, which is the difference between tripping an admission-control threshold and sailing past it.
- Complexity: O(n) after grouping, O(peak window count) space per active variant. Evict variants with no event in the last 60 seconds so the live map stays proportional to concurrently active variants rather than to the two-million-variant catalogue.
Follow-up
- Events can arrive up to five seconds late. What do you buffer, what do you emit, and what happens to an event that is twenty seconds late?
- You need peaks over both 60-second and 5-minute windows. Do you pay twice?
- This result feeds a token bucket that throttles the variant. Describe the feedback loop you have just created.
Parse a supplier feed into minor units and packed GTIN-14
A supplier feed delivers one record per line: sku_code, price as a decimal string, currency_code, and gtin as 8, 12, 13 or 14 digits or empty. Produce list_price_minor as an integer, the currency, and a gtin packed to exactly 14 characters, rejecting any record you cannot represent exactly. Up to one million records per run, mixed currencies. No floating point anywhere on the path. State what you reject and why, and what your rejections cost if nobody reads them.
Approach
- Parse the price as text, never as a number. Split on the single decimal separator and strip a thousands separator only when the feed's format is declared: a bare
1,234is ambiguous between 1234 and 1.234 and must be rejected rather than guessed, because guessing wrong is a factor-of-1000 price error that passes every type check downstream. - Scale by the currency's ISO 4217 minor-unit exponent, which is not always 2 — it is 0 for JPY and KRW and 3 for BHD, KWD, OMR and TND. Right-pad the fraction with zeros to that exponent; if the fraction carries more digits than the exponent allows, reject rather than round, because a supplier sending three decimals for a two-decimal currency has a unit bug rather than a rounding need.
- Say the floating-point number out loud instead of gesturing at it: in binary64,
19.99 * 100evaluates to 1998.9999999999998, soint(19.99 * 100)is 1998 — one cent low, on a path that never raises. Integer digit-string arithmetic makes that class of defect unreachable rather than merely unlikely. - Normalise the GTIN by left-padding with zeros to 14, then validate: weight the 13 payload digits 3 and 1 alternating from the rightmost payload digit, sum, and require the 14th digit to equal
(10 - sum mod 10) mod 10. Left padding is safe precisely because weights are assigned from the right and leading zeros contribute nothing, so a GTIN-13 and its padded GTIN-14 carry the same check digit. - Keep
gtinnullable and out of every join key. It is NULL for own-brand items by design, sosku_code— unique and supplier-facing — is the key the feed applies against, whilevariant_idstays internal and is never exposed to the supplier. - Complexity: O(total input bytes) time, O(1) working memory per record. Route rejections to a quarantine file carrying the field and the reason, and count them per supplier: a silent skip is how a feed loses three percent of a catalogue without anyone noticing.
Worked solution 15 min
- Write the exponent table for four currencies that differ, and name where the values come from.
- Implement string-only parsing and run it over
19.99,0.005,1234,1,234,19.990, and a JPY record containing a decimal point. - Implement the check digit and test it against both a padded and an unpadded form of one known-good GTIN-13.
- Diff your parser against a deliberately naive float implementation over 100,000 generated prices and count the disagreements.
Follow-up
- The same SKU appears twice in one run with different prices. Which wins, and what does that answer depend on?
- A supplier starts sending a currency with a 3-digit minor unit. What in the pipeline breaks first?
- How do you apply a million-row feed without starving the change-event stream its consumers depend on?
Keep the search read model correct when events arrive out of order
Search serves 10,000 queries per second over two million variants with filters, facets and sorting. The origin tables are product_variant, a price table, and one inventory_position row per variant per node. Design the denormalised row the index is built from, justify it from that read pattern rather than from convenience, and write the upsert a consumer runs. Catalog, pricing and inventory publish independently, each with its own monotonic per-row counter, and delivery is at-least-once. State exactly what your upsert does with an event older than the row it hits.
Approach
- Justify the denormalisation with arithmetic rather than with the word performance. Every query filters, facets and sorts, so a live join would hit three sources, and availability alone is a GROUP BY over one inventory_position row per node per variant. At 10^4 queries per second that join is not affordable per request; at 10^2 it would be, and saying so is the difference between a judgement and a habit. One precomputed row per variant is what the read pattern forces.
- Pin the grain and state its cost. One row per variant means a product with forty colourways floods the first page; collapsing to product grain means the displayed price is an aggregate — the cheapest variant satisfying the active filters, which is a different number from the cheapest variant overall. Pick one and make the facet counts agree with it, because counts computed before the filter and results produced after it is a correctness defect, not a ranking preference.
- Reject a single version column. Three producers with independent sequences cannot share one counter: gating a price update with catalog's version either discards good prices or admits stale ones, depending on which producer happens to be ahead. Carry catalog_version, price_version and inventory_version, each gating only the columns its producer owns.
- Make a stale event a no-op rather than an error: INSERT ... ON CONFLICT (variant_id) DO UPDATE SET ... WHERE variant_search_row.catalog_version < EXCLUDED.catalog_version. Then state the consequence the consumer must handle, because it is the part teams miss: a conflict suppressed by that WHERE affects zero rows and RETURNING yields nothing, and a consumer that reads zero rows as failure will retry that event forever.
- Decide availability semantics before storing them. in_stock_any_node is a hint for filtering and ranking and never a promise; the binding decision stays the conditional write against inventory_position. That makes seconds of staleness acceptable here, and makes the worst outcome a customer reaching a variant that turns out to be gone — a refusal at reserve time rather than an oversell.
- Set freshness budgets by consequence rather than uniformly. Price and availability must propagate within seconds because they are user-visible and materially wrong when late; synonyms, boosts and relevance weights ride the next full rebuild, which doubles as the recovery path for a consumer that mis-applied events for an hour.
Follow-up
- A consumer lags an hour and then replays from its offset. What does your version guard do, and what is still wrong after the replay finishes?
- Out-of-stock variants: disappear, sink, or render with a restock date? Give the answer and say what each choice does to facet counts.
- A repricing job emits 200 events per second for one variant for ten minutes. What do you change in the consumer so the rest of the index does not fall behind?
Explain why the reservation expiry sweeper stopped using its index
inventory_reservation has 400 million rows and an index on (state, expires_at_utc). The sweeper runs SELECT reservation_id FROM inventory_reservation WHERE state = 'held' AND expires_at_utc < now() ORDER BY expires_at_utc LIMIT 500 FOR UPDATE SKIP LOCKED, then updates those rows to 'expired'. The backlog of expired held rows never drops below a few thousand, so the LIMIT is always satisfiable. It ran in 4 ms for months; during a promotion it went to 9 seconds and EXPLAIN now shows a sequential scan. Give four distinct explanations consistent with those facts, say how you would distinguish them, and give the index you would ship.
Approach
- Kill the selectivity answer first, because it is the one everybody reaches for and it does not survive the LIMIT. state = 'held' is an equality on the leading column and expires_at_utc < now() is a range on the second, so the scan opens at the start of the held group and walks forward already ordered by expires_at_utc; the range bound is a stop condition, not a filter. It touches roughly 500 index entries and 500 heap tuples whether held is 0.2 percent of the table or 40 percent of it. The competing plan has to sort 400 million rows before it can emit the first one, because Sort is blocking and a LIMIT cannot discount a startup cost. No distribution of held makes that cheaper. So a sequential scan here means the ordered path became unavailable, not that it was outbid, and every explanation worth having is about availability.
- First explanation: the planner cannot see a usable index. A CREATE INDEX CONCURRENTLY or REINDEX CONCURRENTLY that failed or was cancelled under promotion write load leaves pg_index.indisvalid = false, and the planner ignores such an index entirely while every write still maintains it, so write cost looks unchanged while reads fall off a cliff. Same class: a migration dropped or renamed it, or the table was partitioned and the index was only created on some partitions. Distinguish with indisvalid and indisready in pg_index, and with pg_stat_user_indexes.idx_scan, which stops incrementing at a timestamp you can line up against the deploy log.
- Second explanation: the statement stopped matching the index. Three shapes, all of which remove the ordered path rather than outrank it. The sort key or the range got wrapped — date_trunc('minute', expires_at_utc) < now(), or expires_at_utc::date — and no plain B-tree serves an expression. The leading column got wrapped or made collation-sensitive — state::text = $1, lower(state) = 'held', state LIKE 'held' under a non-C collation — so the equality that anchors the scan is gone and the index can neither bound nor order. Or the sweep was widened to state IN ('held','releasing'), which replaces that single equality with an array qualifier; whether nbtree can still return such a scan in index order is version-dependent (its array handling was reworked in PostgreSQL 17), so read EXPLAIN on your own version instead of asserting either way. Distinguish by diffing the normalised text in pg_stat_statements against what you believe is deployed, then EXPLAIN each candidate form side by side.
- Third explanation: the planner was not allowed to pick an index scan. enable_indexscan = off is the setting that reliably produces this shape; note that a merely hostile random_page_cost is not, for the same reason the selectivity story failed — the LIMIT means only about 500 random fetches are ever costed, so multiplying their price does not reach the cost of a full sort. Such a setting arrives from pg_db_role_setting, a connection pooler's startup options, or an ORM's per-session setup, which is why you read pg_settings name, setting, source and sourcefile from the sweeper's own connection rather than from your psql session. Those two sessions differing is the finding.
- Fourth explanation: the 9 seconds and the sequential scan are two incidents, and only one of them is a plan change. The sweeper updates state, which is indexed, so no update on this table can be a HOT update and every sweep leaves a dead index entry behind. Let a long-running transaction or a replication slot pin the xmin horizon and autovacuum cannot reclaim them, so the ordered scan walks a growing prefix of invisible entries to find 500 live ones, and FOR UPDATE SKIP LOCKED makes it read past every row the other sweeper instances hold on top of that. That is seconds of work with the index still in the plan, which means it explains the timing but not the EXPLAIN output, and saying so out loud is the point. Distinguish with n_dead_tup against n_live_tup, pg_stat_activity ordered by xact_start, pg_replication_slots.xmin, and EXPLAIN (ANALYZE, BUFFERS) buffer counts weighed against an output of 500 rows.
- Ship CREATE INDEX CONCURRENTLY ON inventory_reservation (expires_at_utc) WHERE state = 'held'. Moving the state predicate out of the key and into the index predicate leaves a key that is ordered by expires_at_utc globally, so the ORDER BY no longer depends on an equality surviving in the WHERE clause — and if the sweep is widened later, you widen the index predicate to WHERE state IN ('held','releasing') and keep the ordering, rather than losing it to an array qualifier in the key. It holds only rows the sweeper wants, so the walk never steps over committed or expired entries, and it shrinks as rows leave held, which bounds the dead prefix from the fourth explanation without removing it. Two preconditions: the planner must prove the query's predicate implies the index's at plan time, so 'held' has to reach it as a literal and not as a bound parameter inside a generic plan; and expires_at_utc must stay unwrapped in both the predicate and the ORDER BY. Say plainly that this index does not fix bloat — only vacuum and releasing the xmin horizon do.
Worked solution 40 min
- Load 5 million rows with expires_at_utc spread over the past week and state = 'held' on 0.2 percent of them. Run the sweeper query under EXPLAIN (ANALYZE, BUFFERS) and record the plan, the wall clock and the buffer counts.
- Flip 40 percent of the table to held with past expiry times, run ANALYZE, and re-run. Record the same three numbers and compare them against step one.
- SET enable_indexscan = off and enable_bitmapscan = off, then re-run, to see the cost and the wall clock of the plan the incident reported — produced without changing a single row.
- Re-run three rewrites against the composite index and record which lose the ordered path on your version: date_trunc('minute', expires_at_utc) < now(); state::text = 'held'; state IN ('held','releasing').
- Drop the composite index, start CREATE INDEX CONCURRENTLY to rebuild it, cancel it mid-build with pg_cancel_backend, then check pg_index.indisvalid, re-run the sweeper's EXPLAIN, and measure inserts per second.
- Hold an idle-in-transaction session open, run 200,000 sweeps, then re-run EXPLAIN (ANALYZE, BUFFERS) and record n_dead_tup, pg_relation_size and the buffer count for a 500-row result. Close the transaction, VACUUM, and record them again.
- Create the partial index concurrently and repeat steps two and four against it, then run it once more through a prepared statement with state = $1, executed at least six times.
Follow-up
- SKIP LOCKED is doing real work here. What happens to the ORDER BY and the LIMIT when 400 of the top 500 rows are already locked by a second sweeper?
- A committed reservation must never be expired. Where does that guard live so a second sweeper instance cannot break it?
- The sweeper's UPDATE is now the slowest statement in the system. Would you batch it differently, and what does each batch size do to replication lag?
Find the first recurring item in an array.
Find the first recurring item in an array.
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
- What assumption would you test first?
- How would you know your answer was wrong?
Write a function that adds two numbers.
Write a function that adds two numbers.
Approach
- Say what you would check first and why it is the highest-information step.
- 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?
Make cart line edits safe for a retrying mobile client
The caller is a mobile app on a cellular network that retries any request it does not hear back from within three seconds, and it cannot distinguish a lost request from a lost response. Today POST /carts/{cart_id}/lines with {variant_id, quantity} appends or increments server-side, so a retry silently doubles what the customer sees. Redesign the cart mutation contract: the verbs, the resource paths, who mints the identifiers, and the concurrency control. For each design, state what a retry after a lost response returns and what the app does with that answer.
Approach
- Name why the current shape cannot be patched: POST is not idempotent by definition (RFC 9110 section 9.2.2) and the body carries a delta, so the resulting state is a function of how many times the request arrived. Any fix has to make the state a function of the request content alone.
- Move line identity to the client: PUT /carts/{cart_id}/lines/{client_line_id} carrying an absolute quantity, where client_line_id is a UUID the app mints when the line first enters the cart. Replaying it converges on the same state and the server stores nothing extra. The cost is real: an 'add one more' button becomes read-modify-write in the app, and two of the customer's own tabs resolve as last-writer-wins on quantity rather than summing.
- Where a delta genuinely is the intent, keep POST but require an Idempotency-Key minted once per button press rather than per HTTP attempt, stored alongside the response with a TTL and a hash of the request body, so a replay carrying a different body is a 409 conflict instead of a silent replay of a stale answer.
- Add If-Match over an ETag for the concurrent-editor case, and state the limit out loud: If-Match does not make a retry safe, because a retry of a request that already succeeded receives 412 and the client cannot tell 'I did this' from 'someone else edited this'. Conditional requests solve concurrency; idempotency keys solve retries; the contract needs both.
- Choose the ETag grain deliberately. A cart-level ETag is one value and makes simultaneous edits to two different lines conflict spuriously; a per-line ETag lets them proceed but gives up any whole-cart snapshot guarantee.
- Return the full cart representation and its new ETag from every mutation, so a retry that lands as a no-op still leaves the client holding the truth without a follow-up GET.
Worked solution 25 min
- Write the three candidate shapes side by side: POST delta, POST delta with an Idempotency-Key, and PUT absolute against a client-minted line id.
- Trace each through the four timeout outcomes: request lost, response lost, duplicate delivered after the first succeeded, duplicate delivered while the first is still in flight.
- Pick PUT-absolute as the default and write the identifier rule, including when the app is allowed to reuse a client_line_id.
- Specify the concurrency story: ETag grain, If-Match required or optional, the status on mismatch, and the client's recovery path from it.
- Work out what 'duplicate delivered while the first is in flight' does in the database: both writes target the same primary key, the second waits on the row lock and then applies, which is only safe because the value is absolute rather than a delta.
Follow-up
- The app is offline for two minutes and queues four edits to the same line. What does your contract do when all four arrive at once, in the wrong order?
- How long do you retain idempotency keys, and what does the server return for a key whose record has aged out?
- A quantity of zero arrives on the PUT. Is that a delete, a validation error, or a stored zero, and which one does the app expect?
Orchestrator heap grows a gigabyte a day at flat order volume
Order orchestration pods are OOM-killed every three days. Resident memory climbs about a gigabyte a day while order rate, request latency, connection count and in-flight saga count are all flat, and a restart clears it with no change in throughput. Orders run at roughly 200,000 a day. Find the leak, prove it rather than guess it, and say what bounds it permanently.
Approach
- Separate heap from off-heap first, because the tooling diverges completely. Compare resident memory against live heap measured after a full collection: if the heap is flat while resident grows, the growth is native — buffers, sockets, mapped files — and a heap dump will show nothing. Here live heap after collection climbs in step, so it is retained objects with a live reference.
- Correlate the growth against a counter, not against uptime. A gigabyte a day over 200,000 orders is about 5 KB retained per order, and growth that tracks cumulative orders rather than concurrency or uptime points at a per-order structure that is created on every checkout and released on some but not all exit paths.
- Take two heap snapshots an hour apart at steady state and diff by retained size, then walk the dominator tree to the GC root rather than stopping at the largest class. A map of saga contexts reports as character arrays and hash-map nodes if you read the histogram, and as one map if you read the dominators.
- Name the root precisely: an in-process map keyed on the checkout idempotency key, holding the saga context and the price quote, with entries removed only on the polling completion path. Orders that complete through the provider webhook take the other exit and never remove theirs. Two exits, one cleanup.
- Fix with a bound rather than with another cleanup call. The durable saga state is already in the database, so this map is a cache: give it a maximum entry count and a TTL slightly above the quote TTL, and make eviction correctness-neutral by reloading from the database on a miss. Adding a removal to the webhook path fixes today's leak and leaves the third exit path, added next quarter, to leak again.
- Verify against the counter that diagnosed it: live heap after collection flat over 24 hours at the same daily order volume, map size pinned at its bound, and the webhook handler still idempotent when its entry has been evicted.
Follow-up
- Eviction now means a webhook can arrive with no cached context. What must the handler read, and what must it not assume?
- If live heap had been flat while resident memory grew, what three native sources would you check and in what order?
- What metric would have paged someone on day one instead of on the first OOM kill?
Day one measures instead of guessing, under a fixed rubric, and the remaining hours are allocated in proportion to the gaps before any studying begins. The allocation is deliberately not renegotiated midweek, because the area that feels worst on day three is usually the one that is moving.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Diagnostic, scored before you study anything
- Sit a 110-minute diagnostic in four blocks: forty-five minutes on two coding problems, twenty-five on one design prompt taken to interface and data model, twenty of short-answer fundamentals, and twenty delivering two behavioural answers aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct and 0 is stuck, grading the artifact rather than how the attempt felt.
- Allocate days two to five in proportion to 3 minus each block's score, write the allocation down, and commit to leaving it alone.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Largest gap: find the boundary rather than the subject
- Split the weakest area into named sub-skills and rate each separately. For coding those are restating the problem, choosing the structure, stating the invariant, turning the invariant into loop bounds, handling empty and single-element input, and accounting for complexity out loud.
- Attempt three items positioned just above where the rating drops off, and for each write the first move you failed to make.
- Re-attempt one of them from blank four hours later with nothing open.
Deliverable: A sub-skill map with the two blocking sub-skills circled.
Practice prompt ↗Practice prompt ↗03Drill the blocking sub-skill by repeating the shape
- Do eight short repetitions of the same shape rather than eight different problems, so what gets practised is the pattern and not the puzzle.
- State the rule you now hold in one sentence, then test it against a case built to break it, a sliding window over an array containing negative values, or a cache-aside read path whose invalidation message is dropped.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Practice prompt ↗Practice prompt ↗04Second gap, plus maintenance on the strongest area
- Run the same sub-skill decomposition on the second-largest gap in half the time.
- Spend twenty-five timed minutes on the block you scored highest, choosing the hardest item you can still finish rather than a warm-up.
- Write whether each area fails you on recall, on setup, or on execution, and set the fix accordingly: repetition for recall, a written checklist for setup, timed work for execution.
Deliverable: A second sub-skill map plus a one-line failure diagnosis for each area.
Practice prompt ↗Practice prompt ↗Worked solution ↗05The gap that is not a skill
- Record one technical and one behavioural answer, then count two things in the playback: seconds before your first clarifying question, and sentences you began without knowing where they would end.
- Practise saying that you do not know, followed by how you would find out, without letting it soften into a guess, and practise stating a complexity or an estimate before being asked for it.
- Redeliver one answer under a hard ninety-second cap, which forces structure ahead of detail.
Deliverable: Two recordings with a counted reduction in time-to-first-question.
Practice prompt ↗Practice prompt ↗06Retest under day-one conditions
- Sit the same 110-minute structure with new prompts of comparable difficulty and score it on the identical rubric.
- For any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write down which single block you would still lose the offer on.
Deliverable: A second scored rubric placed beside the first, with one named remaining risk.
Practice prompt ↗Practice prompt ↗07Full loop under interview conditions
- Run a sixty-minute mock over the two blocks that moved least, with an interviewer briefed to interrupt and change direction mid-answer.
- Write the recovery script for going blank: restate the question, state your assumption, name the first thing you would check.
- Say every rule from the week aloud without reading it, and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive an interruption.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.
What is your experience with Java, Go, or Azure?
What is your experience with Java, Go, or Azure?
Approach
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
How do you resolve disagreements with coworkers?
How do you resolve disagreements with coworkers?
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.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Why do you want to work for Kroger?
Why do you want to work for Kroger?
Approach
- Give the blast radius: what could have broken, and what you measured.
- Pick a story where you made the decision, not one where you watched it.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Tell me about a time you worked with microservices.
Tell me about a time you worked with microservices.
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
- 01
What is your experience with Java, Go, or Azure?
- 02
How do you resolve disagreements with coworkers?
- 03
Why do you want to work for Kroger?
- 04
Tell me about a time you worked with microservices.
Is this an official Kroger interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Kroger. Rounds and questions reflect what candidates have reported, not a process Kroger has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗Is the interview process mostly technical or behavioral?
It is a mix of both. While junior roles may lean heavily toward behavioral and basic logic, more senior roles will involve deeper discussions about system design and your specific technical experience.
PracHub interview research ↗How long does the hiring process typically take?
It varies, but the process is generally designed to be efficient. Some candidates report receiving feedback or even offers within a week, while others may experience a more standard multi-week process.
PracHub interview research ↗Can I expect a remote interview?
Yes, many rounds—including technical screens and manager interviews—are conducted via video calls, though some locations may still conduct in-person interviews for local roles.
PracHub interview research ↗What is the best way to stand out?
Be prepared to talk about how your work has directly impacted the business. Connect your technical accomplishments to the broader goal of serving the Kroger customer.
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