As a Software Engineer at Paragon Energy Solutions, you play a vital role in bridging the gap between advanced energy technology and practical, scalable software implementation. Your work contributes directly to the efficiency and reliability of systems that support Paragon Energy Solutions' energy infrastructure. By developing robust applications and optimizing technical processes, you help the company's teams maintain the performance standards expected in the energy sector.
This position is inherently collaborative and requires a blend of technical precision and domain curiosity. You will often work alongside electrical and systems engineers, translating complex mechanical or thermal concepts into functional software solutions. Whether you are automating diagnostic tools or refining core data systems, your contributions directly impact how Paragon Energy Solutions delivers energy-efficient outcomes. It is a challenging, high-impact environment where your ability to communicate technical concepts to non-technical stakeholders is just as critical as your coding proficiency.
Initial Screening
reportedBefore anything technical happens, someone has to decide which rung of the ladder your loop is calibrated to, and that decision sets the bar for every round after it. It comes from how you describe scope, not from your title, because titles do not convert cleanly between companies. The weak version of the answer is team size and years. The strong version names the largest change you shipped where nobody reviewed the design, what would have broken if you had been wrong, and what you were paged for. Get the level said out loud on this call, because the range and the loop both follow from it.
What to demonstrate
- Whether the scope in your own account maps onto a level the team actually has an opening at, so a mismatch ends the process cheaply rather than after four interviewers have spent a day
- Whether your title needs re-mapping: the same word describes very different amounts of independent decision-making at a twenty-person company and a ten-thousand-person one
- Whether your compensation expectation can be filled at that level in the structure the role pays in, which is why the number gets asked for before any engineer is scheduled
How to prepare
- Write down two changes from the last two years: the largest one you designed with nobody reviewing the design, and the largest one where someone more senior did. Lead with the first when scope comes up, and be ready to say which parts of the second were yours
- Ask which level the loop is calibrated to and what changes at the level above it, then plan your weeks from that answer rather than from the posting
- Settle a total-compensation range beforehand with the split named, base against bonus against equity and its vesting period, so a question about numbers gets a number instead of the word market
Technical Assessment
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
In-Depth Discussion
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
Final Decision
reportedCoding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.
What to demonstrate
- Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
- Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
- Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
- Whether you can say which calls you made alone and which you escalated, and why the line sat where it did
How to prepare
- Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
- Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
- Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap
PracHub editorial advice for the preparation topics above.
Floating-point quantities and implicit unit-of-measure conversions
Binary floating point cannot represent most decimal fractions exactly, so a chain of pallet-to-case-to-each conversions accumulates a residue that a later rounding turns into a unit that was created or destroyed, breaking conservation with no concurrency involved at all. The bug is slow and non-local: it surfaces as a cycle-count variance weeks later, at a node nobody changed, and it is untraceable because the arithmetic looked correct at every individual step. The fix is to hold quantities as integers in the smallest transacting unit, carry the UoM explicitly on every row that carries a number, store conversion factors as integers that are versioned in time, and reject a conversion that does not divide exactly instead of rounding it. The same discipline applies to money on the freight side, where an accessorial in floating-point dollars produces invoice reconciliation differences of a cent that cost more to investigate than the freight.
Treating the database as the truth about physical stock
Every system in this domain drifts from reality, through miscounts, unrecorded damage, mis-scans and theft, so a design with no adjustment path forces the drift somewhere worse. The usual shape is a CHECK constraint or application guard that clamps a position at zero: the negative that would have exposed a missing receipt is silently swallowed, and the first visible symptom is a picker standing at an empty location for an order the system promised. The correct posture is to let the invariant fail loudly - record the cycle-count adjustment as a first-class movement with its own reason code, surface the variance, and let planning see that this location is unreliable - because an adjustment that is indistinguishable from a normal pick destroys the only signal that would have found the root cause. A related version of the same mistake is deleting or updating a movement row to fix a mistake, which leaves the position correct and the history a lie.
Quoting amortised or average cost as if it were a worst-case guarantee
Appending to a dynamic array is amortised O(1), but the append that triggers a resize copies every element, and hash lookup is constant only while the hash spreads the actual keys. Say which guarantee you are offering when the caller cares about the latency of one call rather than the total over many.
A queue or buffer with no bound
Every producer-consumer boundary needs a capacity and a policy for reaching it: block the producer, shed load, or drop the oldest entry. Unbounded buffering converts a temporary slowdown into memory exhaustion and hides the backpressure signal that would have revealed the consumer was falling behind.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Find the busiest ten-minute window per reader after deduplication
One day of observation_event from fixed readers: source_id (reader), subject_id (container), occurred_at, received_at, dedupe_key. 300 million rows, partner and device replays resend whole windows, and some events arrive six hours after they occurred. For each reader, return the 10-minute event-time window containing the most distinct containers, with the window's start. Target linear work per reader after sorting. State the window semantics you chose, and what the job does with an event that lands after its window was already reported.
Approach
- Deduplicate before windowing, on
dedupe_key, and keep the two concerns separate. A replayed batch is a no-op only if the duplicate is removed first; deduplicating inside the window logic counts a resend as traffic and reports a congestion peak that never happened. Dedupe is a hash set per reader-day or a sort-unique on the key — the same sort you need anyway. - Partition by
source_idand sort each partition byoccurred_at, never byreceived_at. Arrival order is an artefact of device connectivity and partner batch cadence; a reader that went offline for six hours would otherwise show its entire backlog as one impossible burst. Sorting dominates: O(p log p) per reader of size p, O(n log n) overall. - Two pointers over the sorted partition with a hash multiset
container -> countand a separatedistinctinteger. Advance the right pointer, incrementing the count and bumpingdistinctonly on a 0-to-1 transition; advance the left pointer whileoccurred_at[left] <= occurred_at[right] - 600s, decrementing and loweringdistinctonly on a 1-to-0 transition, and erasing the key from the map so the map's size anddistinctnever diverge. Each element enters and leaves once: O(p) amortised after the sort, O(window occupancy) space. - Justify the candidate set rather than scanning every possible start. With half-open windows
[t, t+600), sliding a window right until its left edge coincides with an event can only add events and never removes one, so the maximum is attained at some event'soccurred_at; checking the p windows anchored at events is sufficient and exhaustive. State the half-open convention explicitly — an event exactly att+600belongs to the next window, and flip-flopping on that produces off-by-one disagreements between this job and whatever reads its output. - Handle lateness by choosing a policy, not by hoping. Either run over a closed event-time batch with a stated lateness allowance — the day plus six hours, so the six-hour tail is inside the batch and the day-late tail is not — or emit under a watermark and restate the affected windows when a late event lands. Both are defensible; what is not defensible is silently dropping the late event, because that is how the busiest window quietly becomes the one with the best connectivity.
Follow-up
- Two events for one container arrive from two readers a second apart. Is that congestion, a misread, or one container passing two portals?
- You need this continuously rather than as a daily batch. What changes, and where does the memory go?
- A device's clock is three hours fast. What does that do to this result, and how would you detect it from
clock_offset_ms?
Split an order across the fewest nodes that can fill it
A fulfilment order has L lines, L up to 25. After filtering by capability, service time and stock, N candidate nodes remain; N is usually under 14 but can reach 200. Each node covers a known subset of the lines in full. Return a minimum-size set of nodes covering every line, tie-broken by total distance. Give an exact algorithm for the small case with its exact cost in operations and bytes, say precisely why enumerating node subsets stops working, and give what you ship when N is 200.
Approach
- Name it: this is minimum-cardinality set cover, which is NP-hard, so the useful move is choosing which parameter you are exponential in rather than hunting for a polynomial exact algorithm. Enumerating subsets of nodes is 2^N and correct — at N = 14 that is 16,384 subsets and finishes instantly, which is exactly why it survives review and then fails in production at N = 200, where 2^200 is about 1.6e60.
- Be exponential in L instead, because L is the parameter you bound. Index the DP by the set of lines still uncovered:
dp[rem]is the cheapest way to cover the lines inrem, withdp[0] = (0, 0)and the value a(node_count, total_distance)pair compared lexicographically. Letbbe the lowest set bit ofrem; any cover must contain some node that stocks lineb, so the transition ranges only over those nodes:dp[rem] = min over v with b in cover(v) of dp[rem & ~cover(v)] + (1, dist(v)). Worst case O(2^L * N) — at L = 25 and N = 14 that is 33,554,432 states times 14, about 4.7e8 transitions — and the lowest-bit restriction cuts the real branching factor to the nodes stocking one specific line. - Quote the memory for what you actually store, not for a count array alone. Over 2^25 masks you need three dense arrays:
uint8node count (33.5 MB),uint32total distance (134.2 MB, integer metres so the tie-break compares exactly and the array stays fixed-width), anduint8chosen node (33.5 MB, since N <= 200 fits a byte) — about 201 MB, roughly six times the 33.5 MB that a bare count array would suggest. The same layout at L = 20 is 6.3 MB, which is why this is comfortable up to about 20 lines and needs a deliberate decision at 25. - Reconstruction is free in this formulation and needs no parent-mask array: because every transition eliminates the lowest uncovered line, the predecessor of
remunder the stored choicevis exactlyrem & ~cover(v), so walkremfrom the full mask down to 0 emittingchoice[rem]. If the reachable state count is far below 2^L — coverage sets are usually highly correlated — a top-down memoized recursion over a hash map trades the 201 MB for roughly 40-60 bytes per visited state; measure the visited count first, because a hash map that ends up touching most masks is strictly worse than the dense arrays. Add a branch-and-bound cut ofceil(popcount(rem) / max_v popcount(cover(v) & rem)), which prunes long before the theoretical bound bites. - When exact is out of reach, ship greedy with a stated guarantee: repeatedly take the node covering the most currently-uncovered lines, using bitset popcounts, O(N * ceil(L/64)) word operations per pick and at most min(N, L) picks. Greedy is within H(L) <= ln L + 1 of optimal (Chvatal), and no polynomial algorithm achieves (1 - o(1)) ln L unless P = NP (Dinur and Steurer, 2014) — so effort belongs in the pre-filter and the tie-break, not in a cleverer heuristic. At L = 25 the bound is H(25), about 3.82x worst case, while the realistic gap on correlated coverage sets is zero or one node.
- Close on the objective, because fewest nodes is not cheapest. Two shipments from distant nodes routinely cost more than three from near ones, and a split also costs a second box, a second carrier pickup and a worse customer experience. If cost is the real objective it is weighted set cover — greedy by cost divided by newly-covered lines, same logarithmic bound — and if a node can cover a line only partially, the line mask is no longer sufficient state, since you must track remaining quantity per line; that is the point at which you stop hand-rolling and either restrict splits or hand an MILP to a solver with a time budget.
Worked solution 40 min
- Write
cover(v)as an L-bit mask per node and confirm that the union of all masks equals the full mask, otherwise the order is unfillable and the answer is a backorder, not a cover. - Implement the DP over uncovered masks with the lowest-uncovered-line transition, the
(node_count, total_distance)value and thechoicearray, then measure resident memory and wall time at L = 20 and L = 25 against the quoted 6.3 MB and 201 MB. - Implement greedy separately and run both on 10,000 random instances with L = 12, N = 10, comparing sizes.
- Construct the textbook instance where greedy is strictly worse than optimal — nested sets of geometrically decreasing size plus two disjoint halves — and confirm your greedy reproduces the gap.
- Add the distance tie-break to both and confirm the exact path returns the cheapest minimum-size cover, not merely a minimum-size one.
Follow-up
- The pre-filter is what keeps N small. What is in it, and what happens to your exact path the day someone loosens it?
- One node can cover a line only partially. Show where the bitmask formulation breaks and what state replaces it.
- You have 40 milliseconds inside a checkout call. Which of these runs there, and what runs asynchronously afterwards?
Fold late out-of-order status events into a monotonic leg state
Leg statuses form the chain planned < tendered < accepted < picked_up < in_transit < delivered, with exception and cancelled outside it. You ingest 300 million observation_event rows a day carrying subject_id (leg), observed_status, occurred_at, received_at, clock_offset_ms and dedupe_key; five percent arrive over an hour late and a tail arrives a day late. Write the merge that produces each leg's current status, prove the result depends only on the set of events and not their order, and state the per-event cost.
Approach
- Define the per-leg state as a tuple ordered totally:
(status_rank, occurred_at, source_priority, event_id), and definemerge(a, b)as the maximum under that order. Rank leads;occurred_atand the rest only break ties. The proof is then one line: maximum over a total order is idempotent, commutative and associative, so the state space withmergeis a join-semilattice and the fold over any permutation of the event set yields the same value. That is also why no global sort is needed — the O(n log n) disappears and the fold is O(1) per event, O(distinct legs) state. - Rank before time, deliberately.
occurred_atis device-reported and sometimes wrong; if it led the comparison, the leg's status would be handed to whichever device has the most badly skewed clock. Ranking first means a bad clock can only reorder events within one rank. Correctoccurred_atbyclock_offset_mswhere it was measured, and where the offset exceeds a threshold keep the observation but bar it from winning a tie — the event is still evidence, it just stops being authority. - Make the lattice total, because
exceptionandcancelledare not points on the chain andmaxover a partial order is undefined for incomparable pairs. Modelcancelledas an absorbing top element, and carryexceptionas a boolean flag merged by OR alongside the rank rather than as a rank of its own — otherwise an exception at rank 4 either blocksdeliveredat rank 6 or is erased by it, and both are wrong. A product of semilattices is a semilattice, so the order-insensitivity argument still holds over the tuple. - Deduplicate on
dedupe_keyeven though the merge is idempotent. Idempotence covers a byte-identical redelivery; a partner replaying a window with a freshevent_idis not identical, and the tie-break would let the replay displace the original for no reason. Dedupe on the stable key, then fold — both halves are needed, and conflating them is the usual defect. - Partition the stream by
subject_idso all events for one leg land on one partition: broker ordering guarantees are per-partition only, and while a commutative merge makes interleaving harmless, a concurrent read-modify-write on the same leg row is still a lost update. Where that cannot be guaranteed, make the write a compare-and-set on a version column, or a conditional update that only raises the state. Because the state is mergeable, shard maps combine cleanly and replaying the whole day converges to the same answer — which is what makes reprocessing safe. - Say where genuine regressions live: a delivered shipment that is refused and returns is a new leg, not a rank decrease. Monotonicity is only sound if real reversals are modelled as new objects, and keeping every superseded observation is what lets you answer the dispute later, so the fold discards nothing — it marks losers
superseded.
Follow-up
- An event arrives a week late for a leg already closed and invoiced. Does it change the status, the on-time metric, both, or neither?
- Two events for one leg have the same rank and the same
occurred_atfrom different sources. What decides, and is that decision stable across a replay? - How would you test order-insensitivity rather than assert it?
Model the movement ledger so a retried write is a no-op
inventory_movement is append-only: movement_id, item_id, node_id, lot_id, state, delta_qty (signed BIGINT), uom_code, reason_code, ref_type, ref_id, idempotency_key, occurred_at, recorded_at, reversed_by. inventory_position is keyed (item_id, node_id, lot_id, state) with qty, version, last_movement_id. Give the DDL constraints and the exact statements a pick confirmation runs so that a client which times out after the commit and retries changes nothing. Then say how you correct a pick recorded against the wrong lot two days ago, and why the position carries CHECK (qty >= 0) rather than clamping at zero.
Approach
- Put the guarantee in the schema, not the handler: UNIQUE (idempotency_key) on inventory_movement. A SELECT-then-INSERT cannot work, because two concurrent retries both read nothing before either commits and both then insert.
- Claim with INSERT ... ON CONFLICT (idempotency_key) DO NOTHING RETURNING movement_id, and branch on rowcount. Zero rows means the first attempt already committed, so the handler returns success without touching the position. This branch is the whole point: applying the position update on the conflict path is what makes the ledger and the shelf disagree by one pick forever.
- Keep the insert and the position update in one transaction: UPDATE inventory_position SET qty = qty + :delta, version = version + 1, last_movement_id = :id WHERE item_id = :i AND node_id = :n AND lot_id = :l AND state = :s AND version = :v, then test the rowcount and retry the transaction on zero.
- Use lot_id BIGINT NOT NULL DEFAULT 0 rather than NULL. In a default unique index NULLs are distinct from each other, so a NULL lot would let two position rows exist for the same untracked item and split the running sum in half.
- Correct the mis-keyed pick with two compensating movements, never an UPDATE or a deleted_at flag: one reversal restoring the wrong lot, one fresh pick against the right lot, both with reason_code = 'reversal' or 'pick' and ref pointing at the original movement_id. The original row keeps its values; reversed_by is a back-pointer written in the same transaction so a reader does not need a reverse scan to see that the row was undone.
- CHECK (qty >= 0) aborts the transaction and surfaces a real defect. GREATEST(qty + :delta, 0) writes a plausible number instead, so the missing receipt that caused the negative is never investigated and the first symptom is a picker at an empty location.
Follow-up
- Two retries of the same pick arrive concurrently on different application instances. Walk through both transactions statement by statement and say which one commits a position change.
- occurred_at from a handheld is five minutes ahead of recorded_at because the device clock drifted. Which of your queries break, and which column should each of them actually be using?
- How long do you keep idempotency_key unique for, given the table grows by tens of millions of rows a month?
Write the reconciliation query that finds ledger and position variance
inventory_movement(movement_id, item_id, node_id, lot_id, state, delta_qty) is the ledger; inventory_position(item_id, node_id, lot_id, state, qty, version, last_movement_id, reconciled_through_movement_id) is its running sum, written in the same transaction as each movement. Write one query that reports every key whose stored qty disagrees with the sum of its movements, including keys present in only one of the two tables, bounded by a watermark movement_id you pass in. State what the job does with each variance, and how you avoid re-summing four billion rows every night.
Approach
- Aggregate once and join once: a CTE doing SELECT item_id, node_id, lot_id, state, SUM(delta_qty) AS ledger_qty FROM inventory_movement WHERE movement_id <= :w GROUP BY 1,2,3,4, then FULL OUTER JOIN inventory_position USING those four columns. One pass plus one hash aggregate, O(n) in movements scanned; a correlated subquery per position row instead costs one index scan per key and is orders of magnitude slower at 40 million keys.
- Use FULL OUTER JOIN, not LEFT. A key with movements and no position row is the signature of a lost position update, and a position row with no movements is the signature of a write that bypassed the ledger. Both disappear under a LEFT JOIN from either side.
- Compare with IS DISTINCT FROM and COALESCE the join keys out of both sides, so a missing row reads as a variance rather than as NULL = NULL evaluating to unknown and being filtered away.
- Run both sides in one snapshot: either a single statement, or a REPEATABLE READ transaction. Reading positions in one transaction and movements in another manufactures variances for every key written in between.
- Make it incremental with reconciled_through_movement_id: sum only movements in (reconciled_through, :w] and add that delta to the previously verified total. The catch is that sequence values are handed out before commit, so a movement whose id is below :w can become visible after the scan and would then never be re-summed. Set :w to a max(movement_id) observed a few minutes earlier, beyond the longest write transaction, and full-scan one partition per night on a rotation as a backstop.
- A variance is reported, not repaired. Rewriting qty to match the ledger hides the double-applied retry that caused it; only a physical count may produce a cycle_count_adjust movement, and the location should be flagged unreliable to planning until it is counted.
Worked solution 25 min
- Write the aggregate CTE with the watermark predicate, then the FULL OUTER JOIN and the IS DISTINCT FROM filter, projecting COALESCE(m.item_id, p.item_id) and both quantities.
- Seed a fixture with four cases: agreeing key, disagreeing key, movements with no position row, position row with no movements.
- Run it and confirm exactly three rows come back, with the agreeing key absent.
- Rewrite the same comparison as an incremental sum from reconciled_through_movement_id and confirm it reports the same three keys on a second run after new movements land.
Follow-up
- The job finds 14 variances, all on one node, all negative. What do you look at first, and what would make you suspect the reconciliation job itself rather than the data?
- How do you run this without the nightly scan competing with the ATP read path for buffer cache and I/O?
- What does the query return for a key whose only movements are a receipt and its exact reversal, and is that row worth reporting?
How do you approach debugging a system that involves both software and…
How do you approach debugging a system that involves both software and hardware components?
Approach
- Say what you would check first and why it is the highest-information step.
- Work from the requirement backwards to the design.
- 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?
Can you explain the components and functionality within a stylized dia…
Can you explain the components and functionality within a stylized diagram of the refrigeration cycle?
Approach
- Clarify what is being asked and what a complete answer contains.
- 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?
Design the error taxonomy for an inventory reservation API
The caller is a checkout service working inside a 50 ms budget; on any non-2xx it applies a generic three-attempt retry policy. Design the error contract for POST /reservations on the Availability and Inventory Service. Cover input validation, credentials, insufficient available quantity, a lost compare-and-set on inventory_position.version, quota exhaustion, and a write whose outcome is unknown after a timeout. For each class give the status code, a stable machine code, whether a retry is safe, and whether that retry reuses the idempotency key. Deliver the envelope schema and the classification table.
Approach
- Fix the envelope before the list: application/problem+json (RFC 9457, which obsoletes RFC 7807) with type, title, status, detail and instance, plus two extension members - a stable code and a retry class of never, after_backoff or same_key. Callers branch on the code; the prose stays free to change.
- Separate a business answer from a transport failure. Insufficient availability is a correct, computed answer, so it belongs in the reservation representation as 200 with requested_qty, available_qty and a reason, not behind a 5xx. Dressing it as a server error points every retry layer in the caller's stack at a hot item-node row that will not have stock for hours.
- Classify the rest on two axes - is the resulting position known, and is the caller at fault. Known and caller's fault: 422 for an unknown SKU or a UoM conversion that does not divide exactly, 401/403 for credentials, never retryable. Known and ours: 409 for a lost version CAS, retryable immediately with the same key under a bounded attempt count, because that is contention rather than a bad request. Unknown: 502/504, retryable only with the original key. Overload: 429 with Retry-After.
- Write the unknown rule explicitly, because it is the only class where the caller can do damage: the movement may have committed, so the two permitted actions are replay with the identical Idempotency-Key or resolve with GET /reservations?idempotency_key=..., and issuing a fresh reservation is forbidden.
- Pin the taxonomy with a test asserting every error path emits a code from the enumerated set. Without it the catch-all handler ships an uncoded 500, and the caller's switch falls through to a default branch that is almost always retry.
Worked solution 20 min
- Write the problem+json schema with its two extension members: code (stable enum) and retry_class (never | after_backoff | same_key).
- Enumerate ten concrete failures from the real path: unknown item_id, closed node, half-case quantity, expired credential, key reused with a different body, duplicate key still in flight, quota exceeded, version CAS lost, position store timeout, available_qty below requested_qty.
- Place each in the table with status, code, retry_class and the caller's one-line action.
- Re-read the availability rows and confirm they are 200 responses inside the reservation representation rather than entries in the error envelope.
- Write the caller's switch in pseudocode and check that every branch is reachable from the table and nothing falls through to a retrying default.
Follow-up
- A 409 from a lost version CAS and a 409 from a key replayed with a different body demand opposite caller behaviour. What in the contract tells them apart?
- Your p99 under contention is 120 ms against a 50 ms caller budget. What do you return at the deadline, and which retry class does it carry?
- How do you add a new error code later without breaking a client that switches exhaustively over the current set?
Duplicate bookings and second trailers after a partner slowdown
A partner's booking API ran at 45 s p99 for ninety minutes. Afterwards 214 shipment_leg rows carry two booking references, 41 stops received two trailers, and finance holds duplicate invoices. booking_idempotency_key is generated and persisted on the leg before the call and is sent to the partner, whose documented dedupe window is 60 seconds. The HTTP client retries timeouts three times, and a sweeper re-tenders any leg still in status 'planned' after ten minutes. Explain how duplicates happened despite the key, and give the corrected flow.
Approach
- Establish that the duplicates are timeouts rather than double-clicks. Join the gateway request log to shipment_leg for the 214 legs and check whether each duplicate pair carries the same idempotency key and how far apart the calls landed. Same key, calls minutes apart, first attempt ending in a client timeout is the ambiguous-outcome signature; different keys would mean the key is being regenerated and is a different bug.
- Name why the key did not help. The key was correct and persisted, but deduplication was delegated to the partner, whose window is 60 seconds. A retry at 45 s may land inside it; the third retry does not, and the ten-minute sweeper re-tender certainly does not, so the partner sees a new request and books a second truck. A short partner window plus a long retry horizon is an at-least-once system wearing an at-most-once label.
- Identify the missing state. The leg goes planned to tendered only on a 2xx, so a timeout leaves it exactly as if nothing was sent, which is false: the partner may have committed before the connection dropped. The state machine needs an explicit in-doubt status between requested and confirmed, and nothing may issue a new tender for a leg in that state.
- Replace re-issuing with asking. On any ambiguous outcome, move the leg to in-doubt and let a sweeper resolve it by querying the partner for that key or for bookings against our shipment reference, and only tender again when the query definitively returns nothing. Where the partner offers no query, the manifest or the status feed is the authority and resolution is slower, so the in-doubt state must be allowed to persist rather than being timed out into a retry.
- Move retry ownership out of the process. A retry decorator around an HTTP client disappears on deploy or crash, so the persisted state is the only thing that can drive recovery: a sweeper reading legs in requested or in-doubt, with bounded attempts, backoff and an alert on in-doubt depth. The call site should not retry an irreversible effect at all.
- Deal with the 214 that already exist, since code cannot undo them. Reconcile against the partner's manifest, cancel the duplicate where the cancellation window still allows it, and for legs already executed record the second cost as a compensating entry and dispute the invoice. State plainly that this is a business process with a cost, which is exactly why the guard belongs before the call.
Follow-up
- The partner offers no lookup by your key and no search by reference. What is your resolution path and how long may a leg stay in doubt?
- Which other effects in this system have the same shape and the same missing state?
- How do you test this without a degraded partner, and what does the fault injection have to simulate that a plain timeout does not?
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Practice prompt ↗Practice prompt ↗03Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Practice prompt ↗Practice prompt ↗04Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Practice prompt ↗Practice prompt ↗06One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Practice prompt ↗Practice prompt ↗07Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
For anything that touched live traffic, be ready to say how you would have undone it: a flag, a staged rollout, dual writes with the old path still authoritative. Once the old column is dropped or the source rows are overwritten there is no reverse, so name what you kept a copy of and for how long.
Tell us about a time you had to work in a team.
Tell us about a time you had to work in a team.
Approach
- 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.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
What is your future interest in your current field?
What is your future interest in your current field?
Approach
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Tell us more about yourself.
Tell us more about yourself.
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- 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
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How do you handle ambiguity in a project environment?
How do you handle ambiguity in a project environment?
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Name the disagreement and how you resolved it with evidence.
- 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?
- 01
Tell us about a time you had to work in a team.
- 02
What is your future interest in your current field?
- 03
Tell us more about yourself.
- 04
How do you handle ambiguity in a project environment?
Is this an official Paragon Energy Solutions interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Paragon Energy Solutions. Rounds and questions reflect what candidates have reported, not a process Paragon Energy Solutions has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How difficult are the technical interviews?
The difficulty is generally considered average. The focus is often on your ability to apply your knowledge to practical, real-world problems rather than abstract, high-pressure algorithmic puzzles.
PracHub interview research ↗What if I don't know the answer to a technical question?
Do not panic. Walk the interviewer through your thought process and explain how you would go about finding the answer. They are often more interested in how you approach the unknown than whether you have memorized every fact.
PracHub interview research ↗Is the culture collaborative?
Yes, Paragon Energy Solutions values team-oriented individuals. You will likely be working closely with other engineers, and your ability to communicate effectively is a key component of your success.
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