A Software Engineer at Kelly Services plays a vital role in bridging the gap between complex business requirements and robust technical solutions. As a global leader in workforce solutions, Kelly Services relies on its engineering teams to build and maintain the systems that facilitate talent acquisition, resource management, and seamless systems integration. Your work directly impacts how the organization connects skilled professionals with the companies that need them, making efficiency and reliability core tenets of your daily output.
This role is not just about writing code; it is about understanding the broader ecosystem of systems integration. You will contribute to a fast-paced environment where your ability to translate technical challenges into stable, scalable software directly influences the operational success of the company. Whether you are working on internal platforms or client-facing tools, you will be expected to demonstrate technical precision, a proactive mindset, and a focus on delivering solutions that meet immediate business needs.
The environment at Kelly Services often values speed and directness. Be prepared to discuss how your technical contributions can be implemented quickly to drive immediate value for the team.
Initial Screening
reportedThe title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.
What to demonstrate
- Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
- Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
- Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year
How to prepare
- Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
- Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
- Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter
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
Management Review
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.
Depending on a client field because it is always populated
It is populated by their operational process, not by a constraint in their database, and their process changes without telling you. When it stops being populated, a permissive pipeline writes nulls or defaults into derived data and the damage spreads for weeks before anyone notices. Validate at the boundary and fail the run with failure_class='schema_mismatch' plus a redacted sample, so the failure is loud, attributable to the client's change, and cheap to diagnose. A run that fails clearly is far cheaper than a backfill that has to reconstruct which rows were silently wrong.
Treating an ambiguous failure as a definite one
A timeout, a 502 from an intermediary, or a connection reset after the request bytes were sent all leave the target's state unknown. Classifying those as failures and retrying duplicates the effect; classifying them as successes and advancing the watermark loses data silently. Both wrong answers are common because the ambiguous case is rare in a sandbox and routine in production. The run needs a distinct ambiguous state, a dedupe key that makes the retry safe, and a reconciliation read against the target when the key alone cannot settle it.
Retrying a write that is not safe to repeat
A timeout tells you nothing about whether the server applied the write, so a blind retry of a create or a charge can duplicate it. Either make the operation idempotent, with a caller-supplied key the server deduplicates on or a conditional update, or do not retry it; and use exponential backoff with jitter so the retries of many clients do not synchronise into a second outage.
Assuming the bug is in the framework
Suspect your own code first: read the stack trace top to bottom, check which versions are actually installed rather than which ones you believe are, and reproduce in isolation before blaming a library that thousands of people run daily. When the fault really is upstream, you need that minimal reproduction to say so credibly anyway.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Find peak window utilisation and the first rejected request
One connector binding's requests against a client endpoint arrive as timestamps t[0..n-1] in milliseconds, sorted non-decreasing, n up to 1,000,000. The endpoint's budget is R requests per rolling window of W milliseconds, and a request at time t is admitted only if strictly fewer than R already-admitted requests fall in the half-open window (t-W, t]. Return (a) the maximum number of requests falling in any W-millisecond window, counting every request regardless of admission, and (b) the index of the first request the limiter would reject. Both parts in O(n) time.
Approach
- Part (a) is a two-pointer scan: for each right index, advance left while t[right] - t[left] >= W, which leaves the window exactly (t[right]-W, t[right]], and track max(right-left+1). Left is monotone non-decreasing, so total work is O(n) amortised with O(1) extra space. The maximum over all windows of width W is attained at a window whose right edge is a request timestamp, so scanning right edges is sufficient.
- Part (b) is not the same scan. A rejected request consumes no budget, so the window must count admitted requests only. Keep a FIFO deque of admitted timestamps: pop from the front while front <= t - W, admit if the deque holds fewer than R, and return the first index where it does not. O(n) total pushes and pops, O(R) memory.
- Pin the boundary convention before writing either loop. With a half-open (t-W, t] window a request exactly W milliseconds after an earlier one does not see it; switching to a closed window changes the answer at every boundary and is where most disagreements with the client's own limiter come from.
- Note what fixed buckets of width W buy and cost: O(1) memory, but they admit up to 2R inside a span shorter than W, R at the end of one bucket and R at the start of the next. If buckets are unavoidable, halve the bucket width or use a weighted estimate across two adjacent buckets.
- If part (a) already exceeds R, the binding cannot run at this concurrency under any limiter shape. That is a scheduling problem, not a retry-policy problem, and no backoff tuning fixes it.
Follow-up
- Three bindings and four workers share this one endpoint's quota. Where does the counter live, and what is the correct behaviour when that store is unreachable?
- Add full-jitter backoff to the retry path and state precisely what it prevents that plain exponential backoff does not.
- The client documents the limit as 'about 100 per minute'. How would you measure the real one without tripping it in their production?
Top-k longest connector runs from a stream too large to sort
A day of connector_run rows sits in object storage: 200,000,000 rows, unsorted, far larger than memory, and streamable once. Each row has run_id, connector_id, started_at and finished_at, where finished_at is NULL for runs that never reached a terminal state. Return the 200 longest terminated runs by (finished_at - started_at), ties broken in favour of the lower run_id, plus the count of rows with a NULL finished_at. You may not sort the input or hold it in memory. State the time and space complexity you achieve.
Approach
- Keep a bounded min-heap of capacity k=200 ordered on (duration, -run_id). The root is then the weakest retained entry: smallest duration, and on a duration tie the largest run_id, which is exactly the one the stated tie-break should evict.
- Per row: if finished_at is NULL, increment a counter and skip; otherwise compute the duration and push when the heap holds fewer than k, else compare against the root and replace only if strictly better. O(n log k) worst case, O(k) space, and after the heap warms nearly every row costs a single comparison against the root.
- Reject the alternatives explicitly. A full sort is O(n log n) and at 2e8 rows means an external merge sort over hundreds of gigabytes to produce 200 rows. Quickselect is O(n) average but needs the whole array resident, which the constraint forbids.
- Handle the NULL as data, not as an edge case: depending on the language, subtracting from NULL either throws or yields a sentinel that lands at the top of the list. Count those rows and report them, because a spike of unterminated runs is its own signal.
- To parallelise, give each of p shard readers its own size-k heap and merge the p results. The union of per-shard top-k sets contains the global top-k, because an element beaten by fewer than k rows globally is beaten by fewer than k rows in its own shard, so the merge is exact at O(pk log k).
Follow-up
- Now return the top 20 bindings by total run time instead of the top runs. What changes, and how many distinct keys must you hold to make it exact?
- The key space is too large to hold an exact aggregate. What does a Misra-Gries summary with m counters actually guarantee about the counts it returns?
- Rows now arrive continuously and you want a rolling one-hour top-k. Does the bounded heap still work, and if not, what breaks?
Measure credential exposure past engagement close without double counting
For one engagement you have credential_grant rows: (grant_id, principal_id, target_system_id, issued_at, not_after, revoked_at which may be NULL). The cutoff instant T is the engagement's ends_on plus access_grace_days. A grant is live over the half-open interval [issued_at, min(not_after, revoked_at)). There are up to 200,000 grants across target systems. For each target_system_id, return the total wall-clock time after T during which at least one grant was live, plus the disjoint segments that make it up. Grants that overlap must be counted once.
Approach
- Clip each grant to [max(issued_at, T), min(not_after, coalesce(revoked_at, +inf))) and discard any interval whose start is not strictly before its end. O(n), and it removes every grant that already expired inside the engagement window.
- Bucket the survivors by target_system_id and sort each bucket by start. O(n log n) overall, and sorting dominates the whole algorithm.
- Sweep each bucket once, holding one open segment [s,e): if the next start is greater than e, emit [s,e) and open a new segment, otherwise set e = max(e, next_end). O(n) after the sort, O(1) working state, O(number of emitted segments) output.
- Total exposure is the sum of emitted segment lengths, which is the measure of the union. Summing per-grant durations instead answers a different question and is unbounded above by the elapsed wall clock.
- Produce a second, conservative figure that ignores revoked_at and uses not_after alone. revoked_at records that you asked for revocation; whether the client's identity provider honoured it is not something this table knows, and a self-contained token validated offline stays valid to its own expiry regardless.
- If a bucket does not fit in memory, sort externally and sweep the stream, or push end timestamps into a min-heap keyed by end and pop those below the current start. Same O(n log n), memory proportional to the maximum number of concurrently live grants.
Worked solution 25 min
- Set T = 2026-06-30T00:00Z for one target system with four grants: g1 not_after 2026-06-29T12:00Z; g2 not_after 2026-07-01T00:00Z, revoked_at NULL; g3 not_after 2026-07-01T06:00Z, revoked_at 2026-06-30T18:00Z; g4 issued 2026-07-01T06:00Z, not_after 2026-07-02T06:00Z.
- Clip: g1 becomes empty and drops out. g2 -> [Jun30 00:00, Jul1 00:00). g3 -> [Jun30 00:00, Jun30 18:00). g4 -> [Jul1 06:00, Jul2 06:00).
- Sort by start and sweep: g2 and g3 merge into [Jun30 00:00, Jul1 00:00); g4 starts after that end, so it is emitted separately.
- Union = 24h + 24h = 48h = 2,880 minutes, in two segments with a 6-hour uncovered gap between them. Summing the same three clipped intervals per grant instead gives 24 + 18 + 24 = 66h; the 18h excess is exactly the [Jun30 00:00, Jun30 18:00) stretch where g2 and g3 overlap.
- Recompute ignoring revoked_at: g3 now runs to Jul1 06:00, which bridges the gap, and the union becomes the single segment [Jun30 00:00, Jul2 06:00) = 54h.
Follow-up
- Three grants overlap and one of them belongs to an offboarded contractor. What must the sweep emit so exposure can be attributed per principal?
- Which of your two numbers is the real bound on access if the client system validates tokens offline, and what does that imply about issuing TTLs in the first place?
- Run this across 20,000 engagements as a nightly job with a fixed memory budget. What changes in the shape of the computation?
Soft-delete environments without breaking uniqueness or audit history
environment holds one row per provisioned deployment: environment_id, client_id, engagement_id, env_name, region, status, destroyed_at TIMESTAMPTZ NULL. Decommissioned environments stay readable for seven years, and env_name must be unique per client among live environments only, because a client may reuse a retired name. Design the constraint set and the history table, write the query that lists a client's live environments, and say what your design does about connector_run rows whose foreign key points at a destroyed environment.
Approach
- Keep the tombstone as a nullable timestamp rather than a boolean: destroyed_at carries when as well as whether, and enforce live-only uniqueness with a partial index, CREATE UNIQUE INDEX env_live_name ON environment (client_id, env_name) WHERE destroyed_at IS NULL, which indexes only the live subset and so stays small.
- Get the NULL semantics right before you pick a spelling. In a B-tree unique index two NULLs compare distinct by default, so plain UNIQUE (client_id, env_name, destroyed_at) enforces nothing at all among live rows — every live row is unique on the third column purely by virtue of being NULL. PostgreSQL 15's UNIQUE NULLS NOT DISTINCT (client_id, env_name, destroyed_at) does satisfy the requirement: the NULL live rows now compare equal and collide with each other, while retired rows carry distinct destroyed_at timestamps and stay distinct, so a name can be retired and reused any number of times.
- Prefer the partial index anyway, and be able to say why rather than asserting it. It indexes only the live subset — tens of rows per client — instead of seven years of tombstones; it needs no version floor; and it keeps destroyed_at out of the key, so it cannot be defeated by two rows retired at the same instant. That last case is reachable: now() is transaction-start time, so retiring a name, recreating it and retiring it again inside one transaction writes two identical destroyed_at values and raises a duplicate-key error under the constraint form. clock_timestamp() avoids it if you take that route.
- Put history in a separate append-only table written by an AFTER INSERT OR UPDATE OR DELETE trigger holding environment_id, changed_at, changed_by, op and the before and after row images. PostgreSQL has no system-versioned tables, so this is a trigger or a logical-decoding consumer, not a DDL flag, and the soft delete arrives at that trigger as an UPDATE rather than a DELETE.
- State the foreign-key consequence rather than leaving it implicit: a soft delete leaves every referencing connector_run row resolvable, which is exactly what the audit requirement wants, but it means no read is protected by the database. Give the predicate one home, a view or an RLS-backed view that every caller uses, instead of repeating destroyed_at IS NULL across hundreds of call sites.
- Size the retention: seven years of history plus tombstoned rows in the hot table. Partition the history table by month so expiry is a DETACH PARTITION rather than a DELETE that has to be vacuumed, and decide separately whether a residency-driven erasure request can be honoured against an append-only store at all.
Worked solution 20 min
- Write the DDL with destroyed_at TIMESTAMPTZ NULL plus the partial unique index on (client_id, env_name) WHERE destroyed_at IS NULL.
- Insert two live rows sharing (client_id, env_name) and confirm the second is rejected; set destroyed_at on the first and confirm the insert now succeeds.
- Build the same table a second time under UNIQUE NULLS NOT DISTINCT (client_id, env_name, destroyed_at) and confirm it passes the same two tests, so you are choosing between two working designs rather than rejecting a broken one.
- Add environment_history (history_id, environment_id, changed_at, changed_by, op, row_before JSONB, row_after JSONB) and the AFTER trigger that populates it.
- Write the live-listing query: SELECT environment_id, env_name, region, status FROM environment WHERE client_id = $1 AND destroyed_at IS NULL ORDER BY env_name.
Follow-up
- What stops the next endpoint from forgetting the destroyed_at IS NULL predicate, and does that mechanism survive a developer writing raw SQL?
- The history table stores row images as JSONB and the base table gains a column. What do old rows now mean, and how do you read them six years later?
- A residency erasure request requires a genuine hard delete for one client. What breaks, and what do you keep?
Return the latest attempt per connector idempotency key
connector_run stores one row per attempt: run_id, connector_id, idempotency_key, attempt_no SMALLINT, status (queued, running, succeeded, failed, ambiguous, cancelled, abandoned), failure_class, watermark_from, watermark_to, started_at, finished_at, unique on (connector_id, idempotency_key, attempt_no). For one connector_id, write the query returning the highest-numbered attempt per idempotency_key, keeping only those keys whose latest attempt is not succeeded, ordered by watermark_from. Give the index that serves it, and say what the plan does without that index.
Approach
- Use DISTINCT ON (idempotency_key) with ORDER BY idempotency_key, attempt_no DESC. PostgreSQL requires the ORDER BY to lead with exactly the DISTINCT ON expressions, and that requirement is what makes the row picked deterministic rather than whichever the scan happened to reach first.
- Apply the not-succeeded filter outside, in a wrapping SELECT. Putting it in the inner WHERE changes the question from 'keys whose latest attempt failed' to 'keys that have any non-succeeded attempt', which reports a key that failed twice and then succeeded, and is the single most common wrong answer here.
- Know the portable alternative: row_number() OVER (PARTITION BY idempotency_key ORDER BY attempt_no DESC) in a subquery, filtered = 1 outside. A window function cannot be filtered in WHERE because WHERE is evaluated before the window, and PostgreSQL has no QUALIFY clause, so the subquery is mandatory rather than stylistic.
- Index (connector_id, idempotency_key, attempt_no DESC). With connector_id fixed by equality, the index supplies the exact order DISTINCT ON needs and the Sort node disappears. It does not reduce rows read: through PostgreSQL 17 there is no loose index scan, so the executor still walks every attempt in range rather than skipping to each next distinct key.
- Give the complexity honestly: without the index the plan is a scan plus an O(n log n) sort over the connector's attempts; with it, O(n) ordered reads and constant working memory, where n is attempts in range, not distinct keys. If the distinct keys are a tiny fraction of attempts, a recursive skip-scan is the next step.
Follow-up
- attempt_no is not monotonic in time because a retry was queued out of order. Does your query still pick the right row, and which column should it actually order by?
- How would you return the latest attempt per key across all connectors in one environment without the query degrading to a full sort?
- An ambiguous latest attempt means the target may or may not have been written. What does this query feed, and should ambiguous be treated as failed here?
Can you describe a challenging technical problem you solved in your la…
Can you describe a challenging technical problem you solved in your last role?
Approach
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
What is your current availability, and are you prepared for an immedia…
What is your current availability, and are you prepared for an immediate start?
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Can you walk me through your previous experience and the specific tech…
Can you walk me through your previous experience and the specific technologies you have utilized?
Approach
- 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.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Paginate an append-only credential audit log for client auditors
credential_grant is append-only apart from revoked_at and revocation_reason, and holds grant_id, engagement_id, principal_id, target_system_id, scopes, issued_at, not_after, revoked_at and issued_by_request_id. A client's compliance tool exports every grant for one engagement over the contracted retention period. It runs nightly, is single-threaded, and restarts its export from scratch whenever a page fails. Rows are inserted continuously while it reads, and the largest engagement has roughly four million rows. Design the list endpoint: ordering, cursor, page-size contract, and what the caller does when a page fails or its cursor is rejected.
Approach
- Order on a total order and page by keyset, not OFFSET. issued_at alone is not unique, so the sort key is (issued_at, grant_id): WHERE engagement_id = $1 AND (issued_at, grant_id) > ($2, $3) ORDER BY issued_at, grant_id LIMIT $n, served by a composite index on (engagement_id, issued_at, grant_id). The row-value comparison matches the index prefix, so each page costs an index descent plus the page. OFFSET does O(offset) server-side work per page because those rows are generated and discarded, which makes the whole four-million-row export quadratic in the number of pages.
- Make the cursor opaque and self-describing: the sort key values, a fingerprint of the filter and ordering, and a cursor format version. If the caller changes engagement_id or the sort direction mid-scan, the fingerprint mismatch is a 400 with its own code rather than a quietly wrong page. Opaque also means the caller cannot hand-assemble one, so you can change the encoding later without a version bump.
- Decide what 'every grant' means while rows are arriving. Pin an upper bound at page one: return an as_of timestamp and filter every subsequent page on issued_at < as_of, so the export is a consistent prefix and the next night's run resumes from the previous as_of. Ascending order with an upper bound makes concurrent inserts invisible to a scan in progress; a descending scan without one has every insert shift the window under it.
- Say out loud that mutable columns break the snapshot. revoked_at can be set after a page was served, so an export can carry a row whose revocation the client never sees. Put it in the contract: the export is grants issued within a window with revocation state as of as_of, and a client that needs final state re-reads by grant_id. Do not let a keyset scan imply a consistent snapshot of fields that still change.
- Cap page size server-side with a documented default, and return the next cursor as a link rather than a page number. Do not return a total count for a four-million-row filter; an exact count is a full index scan on every call. If the caller wants progress, it already has the as_of bound and the timestamp in its last cursor.
- Define failure handling. A GET with a cursor is idempotent, so a failed page is safe to retry verbatim. A rejected cursor is terminal and the caller resumes from the last cursor it durably consumed, which is why the contract must tell it to persist the cursor rather than a page number.
Worked solution 25 min
- Write the page query with the row-value comparison and the index it requires, then state which index columns the predicate and the ORDER BY each consume.
- Compute the work for page 4,000 at 1,000 rows per page under OFFSET and under keyset, and write both numbers down.
- List the cursor payload's fields and name the specific failure each one prevents.
- Walk one interleaving on paper: page 3 served, two hundred rows inserted with issued_at below as_of, page 4 requested. Say exactly which rows the caller sees.
- Write the contract sentence that covers revoked_at changing after its row was exported.
Follow-up
- The compliance tool is down for three days and does not want to re-read what it already has. What does it store, and what does the endpoint need for that to work?
- The caller supplies engagement_id itself. What stops this endpoint returning another client's grants?
Timesheet writes freeze during a month-end migration window
At 23:40 on the last day of the month, every query touching work_record starts timing out: submissions, approvals and the billing generator. Database CPU is near zero and nothing is doing work. pg_stat_activity shows 60 backends with wait_event_type = 'Lock', an ALTER TABLE work_record ADD COLUMN issued at 23:38, and an analytics SELECT over work_record that started at 23:26 and is still running. Explain the mechanism, state precisely what you cancel first and why, and give the permanent guard.
Approach
- Read the wait states before changing anything. Grouping pg_stat_activity by wait_event_type and seeing 60 backends in 'Lock' with idle CPU says queueing, not load: nothing is slow, everything is waiting, and no amount of capacity helps.
- Walk the chain to its root with pg_blocking_pids(pid) rather than inferring from start times. You will find ordinary reads and writes blocked by the ALTER, and the ALTER blocked by the analytics SELECT.
- State the mechanism exactly. ADD COLUMN takes ACCESS EXCLUSIVE, which conflicts with the ACCESS SHARE the running SELECT holds, so the DDL waits. Lock requests queue, so every request that arrives afterwards queues behind the pending ACCESS EXCLUSIVE, including plain SELECTs that would never have conflicted with the analytics query. One waiting DDL statement freezes the table without ever executing a byte of work.
- Act in the correct order: cancel the DDL first with pg_cancel_backend on that backend. The queue drains immediately and the writes behind it proceed while the analytics query finishes harmlessly. Cancelling the analytics query instead lets the ALTER take the lock and rewrite or at minimum lock the table during the busiest write window of the month, which is worse than the outage you are in.
- Guard permanently at the migration session: SET lock_timeout to a couple of seconds so a DDL that cannot take its lock fails fast instead of queueing the world behind it, and retry the migration later rather than waiting. Know the version-specific escapes too: from PostgreSQL 11, ADD COLUMN with a non-volatile DEFAULT avoids the table rewrite but still requires ACCESS EXCLUSIVE, so no rewrite is not no lock; and where validation needs a scan, ADD CONSTRAINT ... NOT VALID followed by VALIDATE CONSTRAINT keeps the long phase at SHARE UPDATE EXCLUSIVE.
- Cap the other side as well: statement_timeout on the reporting role, so a long read can never become the head of an estate-wide stall, and keep DDL out of the month-end billing burst entirely.
Follow-up
- The migration has to ship this week and month-end runs for three days. What sequence gets it in without a quiet window?
- Now you need a NOT NULL column with a check constraint on the same table. Which step takes which lock, and for roughly how long?
- What signal would have paged you within 30 seconds of 23:38, given that CPU, error rate and query duration all looked normal at first?
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.
Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.
How do you approach explaining complex code structures or technical co…
How do you approach explaining complex code structures or technical concepts to stakeholders?
Approach
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
How do you handle tight project deadlines or urgent requirements?
How do you handle tight project deadlines or urgent requirements?
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.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Report a residency boundary crossing you discovered yourself
You discover that a global sink, such as an error tracker, a central metrics pipeline, an analytics export or a cross-region support tool, has been receiving fields that originated in a restricted residency region, through no feature change at all. Describe a time you surfaced a problem like this: something nobody was asking about, with a contractual dimension, that would have been easier to leave alone. State how much scope you established before escalating, who you told and in what order, and what you changed so the class could not recur silently.
Approach
- Establish scope before raising it, and time-box that work: which fields crossed, roughly how many records, over what period, into which sink, where that sink stores its data and what its retention is. An escalation missing those is noise that spends credibility you will need later.
- Stop investigating alone at the point where the answer changes who must know. A residency crossing usually carries a notification obligation, so the engagement owner and legal enter before your scoping is complete, with the unknowns named explicitly.
- Say what you stopped versus what you left running. Cutting the pipeline can destroy the very records you need for scope, so the usual move is to stop the egress at the source and preserve the sink under legal hold.
- Give a structural fix rather than a cleanup: a redaction allowlist at each egress point instead of a denylist, regionally partitioned sinks with no cross-region data dependency, and a test that fails when a tagged payload field reaches a global sink.
- Name the incentive you were working against, since the finding was yours and it created work for people who had not asked for it, without turning the account into a story about your own integrity.
Follow-up
- Your scoping query would itself read the crossed data out of the global sink. How do you scope without widening the exposure?
- The pipeline was added by another team for a good reason and their change passed review. What did the review process miss, and what would you add that does not slow down every change?
- 01
How do you approach explaining complex code structures or technical concepts to stakeholders?
- 02
How do you handle tight project deadlines or urgent requirements?
- 03
You discover that a global sink, such as an error tracker, a central metrics pipeline, an analytics export or a cross-region support tool, has been receiving fields that originated in a restricted residency region, through no feature change at all. Describe a time you surfaced a problem like this: something nobody was asking about, with a contractual dimension, that would have been easier to leave alone. State how much scope you established before escalating, who you told and in what order, and what you changed so the class could not recur silently.
Is this an official Kelly Services interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Kelly Services. Rounds and questions reflect what candidates have reported, not a process Kelly Services 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?
Candidates generally describe the process as straightforward. The difficulty lies in your ability to clearly explain your past work and demonstrate your technical logic on the spot.
PracHub interview research ↗Is there a coding test?
You should be prepared to discuss code samples or walk through technical problems during the interview. While not always a high-pressure live coding environment, you must be comfortable explaining your thought process.
PracHub interview research ↗What is the typical timeline for an offer?
Due to the immediate nature of many roles at Kelly Services, the process can move very quickly. You should be prepared for a decision shortly after your final interview.
PracHub interview research ↗How can I stand out?
Focus on clearly communicating your previous contributions and showing that you are ready to start immediately. Demonstrating a proactive approach to problem-solving is highly valued.
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