A Software Engineer at RSM plays a vital role in bridging the gap between complex business requirements and scalable technical solutions. As a global audit, tax, and consulting firm, RSM relies on its engineering teams to build, maintain, and integrate the platforms that power its professional services. You will not just be writing code; you will be solving problems that directly impact the efficiency and accuracy of data-driven client deliverables.
This role is both challenging and intellectually stimulating. You will work within diverse, multidisciplinary teams, often involving Workday development, system integration, or custom application builds. Because RSM operates in a highly regulated environment, your work requires a high degree of precision, attention to detail, and a focus on long-term maintainability. You should expect to engage with stakeholders, understand their unique business challenges, and translate those into robust, functional software.
Technical Assessments
reportedMost of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Interviews with Management
reportedAn unlabelled round is first an information problem, and the cheapest information is free. Whoever schedules it can usually tell you how long it runs, who will be in the room and what they work on, whether you will be writing code and in what environment, and whether anything is being sent beforehand. Ask in writing so the answer is on record, then prepare for the two or three formats those answers still leave open instead of betting on one. What separates a strong candidate is not guessing right; it is having an opening that works whichever one it turns out to be.
What to demonstrate
- Whether you can start work from an ambiguous brief, since tolerating a vague scope without stalling is the same thing the job asks for
- Whether the questions you asked beforehand were ones that change your preparation, such as duration, medium and who is joining, rather than ones whose answers you could not have acted on
- Whether you adapt when the round turns out to be something other than what you were told, instead of spending the first ten minutes visibly recalibrating
How to prepare
- Send one short scheduling message asking four things: how long, who is joining and what they work on, whether you will be writing code and where, and whether to prepare anything in advance. Treat a vague reply as real information, since it means the round is loosely structured and you will be shaping it yourself.
- Write one opening that works in any of the formats still open: restate in your own words what you have been asked to do, then ask which of two directions is more useful to them. Say it aloud until it stops sounding recited.
- Set up for the two most likely formats before the call starts, with a blank editor in the language you would choose and a shared document you can type into, so a format surprise costs you nothing in the first minutes
PracHub editorial advice for the preparation topics above.
Forking the product per client instead of building a configuration or extension point
A fork is the cheapest possible answer for one client and the most expensive possible answer for the estate. Maintenance cost grows as O(clients x changes): a single security patch now needs one port per fork, each with its own conflicts, its own test run and its own release window, and the forks drift further apart with every port. The honest rule is that a required fork is a product gap, so it should either become a supported extension point with a stable interface, or be an explicit, time-boxed and separately priced artefact that is never expected to receive upstream fixes.
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.
Saying 'eventually consistent' without naming the anomaly a user would see
Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.
Reading the constraints as preamble rather than as part of the problem
The bounds are usually there to eliminate the obvious approach: n up to 10^5 makes an O(n^2) scan roughly 10^10 operations, far outside any per-test time budget, and an input larger than memory rules out loading it at all. When a bound is not given, ask for it, then say out loud which approach it kills.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Compare release versions and report the live estate spread
Each environment row carries reported_release as TEXT: either NULL, or a version of the form MAJOR.MINOR.PATCH with an optional -rc.N suffix. Up to 400 environments. Ordering rule: compare major, then minor, then patch numerically; a -rc.N build precedes the same MAJOR.MINOR.PATCH with no suffix, and rc numbers compare numerically. Return the lowest and highest reported versions, the minor-version spread between them, and the count of environments whose reported_release is NULL. NULL means the environment's version is unknown, not that it converged.
Approach
- Parse each string once into a comparable tuple: (major, minor, patch, is_final, rc), where is_final is 1 for a plain release and 0 for a pre-release, and rc is 0 when absent. Tuple comparison then yields the stated order for free, including 3.10.0-rc.1 < 3.10.0. O(L) per string, O(n*L) total.
- Track min and max in one linear scan rather than sorting: O(n) time, O(1) extra space, and n is only a few hundred so the parse dominates either way.
- Fail loudly on a string that does not match the grammar instead of defaulting it to 0.0.0 or to the newest release. An unparseable reported_release is a reconciliation-ingest bug, and either default hides it in the direction that looks safe.
- Compute the minor spread only when min and max share a major version. Across majors the difference of minor numbers is meaningless, so return the pair of versions and flag the condition rather than a number.
- Report the NULL count as its own output line. Folding unknowns into 'converged' is how an install ends up running a release nobody knew was still live.
Worked solution 15 min
- Input reported_release values: '3.9.0', '3.10.2', '3.10.0-rc.1', NULL, '3.9.0'.
- Parse to tuples: (3,9,0,1,0), (3,10,2,1,0), (3,10,0,0,1). The NULL is diverted to the unknown counter before parsing.
- Scan for min and max by tuple: min is (3,9,0,1,0) = 3.9.0, max is (3,10,2,1,0) = 3.10.2. Confirm 3.10.0-rc.1 sits strictly between them.
- Same major (3), so the minor spread is 10 - 9 = 1. Unknown count is 1.
Follow-up
- Two majors are live at once. What does 'spread' mean now, and what single number, if any, would you still publish?
- reported_release is a claim from the environment, and last_heartbeat_at may be days old for a client-managed install. At what staleness does this figure stop being usable?
- How would you enforce the supported window when the rollout plan is built, instead of reporting a violation after it already exists?
Compute the minimal re-run set that preserves idempotency keys
One connector binding must cover [span_start, span_end). You have all its connector_run rows: (idempotency_key, attempt_no, status, watermark_from, watermark_to), with half-open ranges, where idempotency_key is derived deterministically from the connector and the range, so re-running a range reproduces its key. Only status='succeeded' counts as covered. Produce the minimal re-run plan: every instant in the span ends up covered, any sub-interval some prior run already claimed is re-run at exactly that claim's range, and genuinely unclaimed sub-intervals become new claims cut at the binding's chunk size. Explain what merging ranges would cost.
Approach
- Merge the succeeded ranges into a maximal disjoint union: sort by start, sweep extending the open segment while the next start is not beyond the current end. O(n log n) time, O(n) space, and this is the only status that contributes coverage.
- Take the complement of that union inside [span_start, span_end) to get the uncovered gaps, in O(u) over the merged segments. Clip at both ends so the plan never reaches outside the span.
- Sweep the non-succeeded claims (ambiguous, failed, abandoned) against the gap list with a two-pointer over both sorted lists. Any claim intersecting a gap is emitted at its original watermark_from and watermark_to, byte for byte, so the derived idempotency_key reproduces and the retry rejoins the same ledger row. O(n) after sorting.
- Subtract the emitted claim ranges from the gaps; whatever survives was never claimed by any attempt and becomes fresh ranges cut at the chunk size. Only these get new keys. O(u + c).
- Accept that a re-emitted claim may overlap time that is already succeeded. Range bookkeeping is not what makes the retry safe; the record-level dedupe key is, and re-reading a covered window costs one extra fetch against the client's quota, which is far cheaper than a key you can no longer reproduce.
- Exclude anything still in status 'running' with a live lease. Re-claiming it races the in-flight attempt, doubles the quota spend against the client endpoint, and produces two ledger writers for one key. Total plan cost is O(n log n) time and O(n) space.
Follow-up
- An ambiguous run may have applied every record before the timeout. What do you read to settle it before re-running, and what is left if the client's schema offers no natural key?
- Retention pruned the ledger rows for one of these keys. What does the re-run do now, and what bounds how long the ledger must be kept?
- Two planners run concurrently against the same binding. What stops them emitting and executing the same plan twice?
Collapse connector retry attempts into logical jobs and failure counts
You are given one UTC day of connector_run rows: (connector_id, idempotency_key, attempt_no, status, failure_class, records_read, records_applied), unique on (connector_id, idempotency_key, attempt_no), up to 5,000,000 rows in arbitrary order. Retries of one logical job share the idempotency_key; the job's outcome is the status of its highest attempt_no. Return, per connector_id: the number of logical jobs, a count of terminally failed jobs by failure_class, and the dedupe hit total, meaning sum(records_read - records_applied) over terminal-succeeded attempts only. One pass over the input; state your memory bound.
Approach
- Key a hash map on (connector_id, idempotency_key) and keep only the highest attempt_no seen so far plus that row's status, failure_class and record counts. One pass, O(n) time, O(d) space where d is the number of distinct logical jobs, not O(n).
- Treat a repeated (key, attempt_no) as corrupt input and raise, since the table's uniqueness constraint says it cannot happen; silently overwriting hides a double-insert in the runtime.
- Fold the d surviving entries into per-connector counters in a second pass over the map, not over the rows. The failure_class histogram is a small fixed-width map per connector because failure_class is an enum.
- Compute dedupe hits only from terminal-succeeded attempts. A failed attempt's records_read is work attempted, not work deduplicated, and adding it counts the same source records once per retry.
- If d does not fit in memory, partition the input by hash(connector_id, idempotency_key) into p files and aggregate each partition independently. Same O(n) total work, p sequential passes, memory traded for I/O; the partition function must use the full key or a job's attempts split across files.
Follow-up
- A job's highest attempt is 'cancelled' but an earlier attempt succeeded. What is the job's outcome, and what does that say about who writes the cancel?
- Produce this incrementally as runs land instead of as a daily batch: what state do you keep per key, and what happens when an attempt arrives out of order?
- records_applied is written by your runtime, not by the client. What would you reconcile it against before anyone trusts the dedupe-hit number?
Add a non-null column to a live work-record table
work_record holds roughly nine million rows and absorbs a month-end write burst. You must add currency CHAR(3) NOT NULL, backfill it from the engagement, add a foreign key to rate_card, and eventually drop the superseded hours_decimal column. Several releases are live, including client-managed installs running two-quarter-old code that still writes hours_decimal. Give the ordered plan, the lock each step takes and for roughly how long, how the backfill is chunked and restarted, and the signal that makes the drop safe.
Approach
- Sequence it expand, dual-write, backfill, switch reads, contract, and be explicit that only the last step is dangerous. Adding is cheap because an old release ignores a column it does not know about; removing breaks a writer you cannot upgrade and whose failure you may not see for weeks.
- ALTER TABLE work_record ADD COLUMN currency CHAR(3) — nullable, no default. That is a catalog-only change on every supported version and takes milliseconds. Refuse NOT NULL DEFAULT 'XXX': PostgreSQL 11 and later store a non-volatile default in the catalog and materialise it on read, so it is just as fast, but it hands all nine million rows a value that is already non-null and already wrong. That destroys the rest of the plan — the NOT NULL you add later is satisfied from the first second, so it can never find a row the backfill missed, and every consumer now has to distinguish a real currency from a sentinel forever. A NULL is the only marker that survives a crashed backfill.
- The statement is instant; the lock queue is not. An ACCESS EXCLUSIVE request waits behind every open transaction holding a conflicting lock on the table, and every request arriving after it queues behind that, so one long analytics SELECT turns instant DDL into a total stall. Run it with SET lock_timeout = '2s' inside a retry loop that catches 55P03 and backs off with jitter.
- Decide up front what an unconverged writer inserts. Two-quarter-old code omits currency, so it writes NULL, which means SET NOT NULL would start rejecting its inserts outright. Either fill the gap in the database with a BEFORE INSERT trigger that resolves currency from the engagement when NEW.currency IS NULL — one indexed lookup per insert, which you should time against the month-end burst before committing to it — or leave the column nullable and gate SET NOT NULL on the same release-convergence signal that gates the drop. What you must not do is paper over it with a default, which turns a visible NULL from an unconverged writer into an invisible 'XXX' that reconciliation will never flag.
- Backfill on a keyset cursor — WHERE work_record_id > $last AND currency IS NULL ORDER BY work_record_id LIMIT 5000 — one transaction per batch, advancing $last to the last work_record_id the batch wrote so filled rows are never revisited. Set statement_timeout, pause between batches so autovacuum and any replicas keep up, and persist the cursor so a crash resumes from the last committed batch. OFFSET n costs O(n) per page because the server scans and discards those rows, which makes the whole backfill quadratic in the table size.
- Take NOT NULL without a blocking scan, and only once the backfill reports zero rows remaining: ADD CONSTRAINT work_record_currency_nn CHECK (currency IS NOT NULL) NOT VALID takes a brief ACCESS EXCLUSIVE and scans nothing; VALIDATE CONSTRAINT runs under SHARE UPDATE EXCLUSIVE so reads and writes continue throughout, and raises check_violation, SQLSTATE 23514, naming a row if the backfill missed one — that failure is the entire reason the step exists, and it only exists because the column was added without a default; then SET NOT NULL, which on PostgreSQL 12 and later proves itself from the validated CHECK and skips its own full scan.
- Use the same shape for the foreign key — ADD CONSTRAINT ... REFERENCES rate_card(...) NOT VALID, then VALIDATE CONSTRAINT — and build any supporting index with CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block, scans the table twice, and on failure leaves an INVALID index that must be dropped concurrently before you retry. Note what the FK does not prove: a single-column reference is MATCH SIMPLE, so rows with NULL currency pass validation untouched, and validating it before the backfill finishes certifies only the rows that were already correct.
- Gate the contract step on observed convergence rather than elapsed time: every non-destroyed environment must report a reported_release_id at or past the release that stopped writing hours_decimal, and reported_release_id IS NULL counts as unknown and therefore blocks. DROP COLUMN itself is a catalog-only update, instant under ACCESS EXCLUSIVE, reclaiming no space until a rewrite — the cost is entirely in the writer you did not know was still running.
Worked solution 45 min
- Write out each DDL statement in order, annotated with its lock mode and whether it is safe during the month-end burst.
- Implement the retry wrapper: SET lock_timeout, attempt, catch 55P03, sleep with jitter, retry a bounded number of times.
- Write the keyset backfill loop with the cursor persisted after each committed batch, and time one batch.
- Leave one row un-backfilled on purpose, run VALIDATE CONSTRAINT, and read the 23514 it raises — then fix that row and re-run. A plan in which this step cannot fail is a plan that is not checking anything.
- Write the convergence query over environment that returns the rows still blocking the drop.
- Ship the DROP COLUMN as its own migration whose precondition is that query returning zero rows.
Follow-up
- A client-managed install reports a release you no longer support and has not converged for two quarters. Do you drop, and what do you owe that client first?
- During the dual-write window, which column does the month-end reconciliation read, and what does a mismatch between the two mean?
- The backfill dies at batch 900 of 1,800. What exactly does the restart re-read, and how do you know it did not skip a range?
Denormalise the entitlement snapshot without freezing the clock
Authorisation currently joins engagement (effective-dated, with status, ends_on, access_grace_days, effective_from, effective_to), entitlement and principal_assignment on every request, at the platform's full read rate, against tables taking a few hundred writes a day. You are asked to publish a denormalised snapshot that callers cache with a few seconds of staleness and invalidate on change events. Specify the snapshot's key and shape, say which columns you denormalise, name the one thing that must not be precomputed, and say what a cache miss does.
Approach
- Justify the shape from the ratio rather than from taste: a few hundred writes a day against the whole estate's read rate is the case for a published read model. The normalised tables stay the system of record; the snapshot is derived, versioned and disposable, and can be rebuilt from source at any time.
- Key it on (client_id, principal_id) with a monotonic snapshot_version and a generated_at. Consumers cache by version, refresh on a change event, and reject anything older than the staleness bound, which turns the bound into an enforced property instead of an assumption about how fast events usually arrive.
- Denormalise only what changes on a write: the open engagement version's engagement_id, status, isolation_tier, data_region, ends_on, access_grace_days, and the resolved scope set. Resolving the effective-dated row once at publish time — selecting the version WHERE effective_to IS NULL — is the entire saving, because that resolution is what the per-request join was paying for.
- Do not precompute is_active or any boolean that folds now() into the stored value. A write event invalidates the snapshot; the passage of time does not, so an engagement that ended an hour ago keeps authorising until something unrelated triggers a republish. Ship ends_on and access_grace_days as data and compare them to the current time at read, so time-based expiry needs no invalidation at all.
- Fail closed on a miss or on over-staleness: deny, and do not fall through to the source tables. A fallback read path means the first mass cache miss converts an availability incident into either an authorisation bypass or a stampede onto the one service whose availability target already exceeds everyone else's.
- State the cost you accepted: a revoked entitlement is still honoured for up to the staleness bound. That is defensible for widening access and not for narrowing it, so pair it with an out-of-band deny signal for revocation and closure, and write the residual window into the runbook instead of leaving it for an auditor to discover.
Follow-up
- How do you detect the snapshot quietly diverging from the source tables, given that a wrong snapshot is silently permissive?
- An amendment creates a new engagement version mid-request. Which version does the in-flight request use, and can you defend that?
- The snapshot carries data_region. What stops the snapshot itself from being replicated somewhere its own residency rule forbids?
How would you contribute to this role?
How would you contribute to this role?
Approach
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
- Work from the requirement backwards to the design.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
How would you reverse a string or check for a palindrome?
How would you reverse a string or check for a palindrome?
Approach
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Make timesheet submission safe under a client's automatic retries
work_record holds work_record_id, engagement_id, principal_id, work_date, entered_at, minutes, is_billable, status, approved_at, locked_at and invoice_line_id, and entered_at is assigned by the server at insert. A delivery-team app submits entries over mobile networks and retries any request that has not answered within ten seconds. Today POST /engagements/{id}/work-records inserts a row and returns 201, and month-end shows duplicated minutes. Design the write contract so a retried submission cannot double-book: start from whether those columns give you a natural key, then specify the key you actually use, its scope and lifetime, the response on replay, the behaviour when the same key arrives with a different body, and what the app does on each outcome.
Approach
- Name the actual fault: POST is not idempotent, and a ten-second timeout leaves the outcome unknown, because the row may have committed and only the response was lost. Retrying is correct client behaviour; making the retry safe is the server's job. Two mechanisms exist and they are not interchangeable — a natural key derived from the row's own grain, or an opaque client-supplied key — so the first question is which one this grain can support.
- Test the natural key against the columns you were given, and reject it. The candidate is (engagement_id, principal_id, work_date), and nothing left over separates two legitimate entries: minutes and is_billable repeat honestly, because one person can book two distinct ninety-minute billable blocks on one day, and entered_at is server-assigned so it differs on every retry — it changes at exactly the moment a dedupe key must stay fixed. A unique index on that triple therefore rejects a real second entry, trading a visible bug for an invisible one: duplicated minutes surface at month-end, while silently refused minutes surface as an under-billed engagement nobody reconciles. Prefer a natural key wherever the grain genuinely has one; this grain does not, so the key has to come from the client.
- Require an Idempotency-Key header and say so in the contract. With no natural key available, a request that arrives without one cannot be made retry-safe by anything the server knows, so refuse it with its own stable code rather than accepting it and hoping. The IETF header field is still a draft, so document your own semantics rather than pointing at a spec. Scope the key to (authenticated principal, endpoint, key) so one tenant cannot collide with another's, store a fingerprint of the request body beside it, and retain it at least as long as the client's maximum retry horizon; a key pruned before the last retry re-opens the duplicate window it existed to close.
- Pin the client's half of the contract, because that is where this design usually fails in practice: the key is generated once per logical entry and reused byte-for-byte on every retry of it. An app that mints a fresh UUID per attempt satisfies the header and still double-books, and no server-side check can tell that apart from two genuine entries. Specify a deterministic construction the app can rebuild after a restart rather than one it holds in memory, or a crash between attempts reintroduces the duplicate.
- Same key with a different body is a distinct 4xx with its own code, never a silent replay of the first response. These are two different bugs: a genuine retry must replay, and a key reused for new content is a client-side key-generation defect that stays invisible for months if you answer it with the original body.
- Handle the concurrent in-flight duplicate, which is the case a single-threaded test never reaches: two copies arrive forty milliseconds apart, both miss the key lookup, both insert. Resolve it in the database rather than by check-then-act. Put a unique index on (principal_id, endpoint, idempotency_key), insert that row in the same transaction as the work record, and let the loser take the violation. Under READ COMMITTED in PostgreSQL the second insert blocks on the winner's uncommitted key row and then raises 23505 when the winner commits; that error aborts the transaction, so take a SAVEPOINT before the insert if you intend to read the winner back on the same connection. Note also that INSERT ... ON CONFLICT DO NOTHING returns no row on conflict, so the handler needs an explicit SELECT afterwards, or ON CONFLICT DO UPDATE ... RETURNING, to obtain the winning row.
- Answer the loser that arrives before the winner has stored its response with a 409 carrying its own code and a Retry-After, not with a guessed body. Store each response with its status code so a later replay is byte-identical, and write the app's branch table: 201 and 200 both mean submitted; the different-body 4xx and the missing-key 4xx stop and surface to the user; a 409 against locked_at means re-read; the in-progress 409 retries after Retry-After; timeouts and 5xx retry with the same key and the same body. No branch may retry a terminal 4xx and no branch may drop a timeout.
Worked solution 30 min
- List the columns that could separate two entries on the same engagement, principal and date, strike out each with the reason it cannot, and write down the legitimate row pair a unique index on that triple would reject.
- Write the handler as one transaction containing both the key row and the work record, and mark the commit point explicitly.
- Enumerate four arrivals of the same request — first, retry after commit, retry after a crash before commit, and a concurrent duplicate whose winner has not yet stored a response — and give each a status code and body.
- Write the different-body case, including how the body fingerprint is computed and which fields are excluded from it.
- State the key's scope and retention, and tie the retention number to the client's retry horizon rather than to a round figure.
- Write the app's branch table and check that no branch retries a terminal 4xx and none drops a timeout.
Follow-up
- The period has been billed and locked_at is set when a retry finally lands. What status does that retry get, and what does the app show the user?
- What would have to be true of work_record for a natural key to be the better answer here, and what would you still keep the header for?
- How long do you keep idempotency keys, and what exactly breaks on the day you prune one too early?
Environment list page slows as the estate grows
An estate console lists environments for an operator: environment_id, client name, isolation_tier, the desired release version string, the reported release version string, and the engagement's current status. A 10-row filter returns in 25 ms; the unfiltered 200-row page takes 640 ms. No single statement exceeds 0.5 ms, and the database records 553 statements for that request. Rows where reported_release_id is NULL are measurably cheaper than the rest. Diagnose the cause and give the fix, stating the statement count you expect afterwards.
Approach
- Get the shape before theorising about plans. Divide the per-request statement delta in pg_stat_statements by the rows returned: 553 statements for 200 rows, none over 0.5 ms, is round trips rather than work, and rules out a bad plan immediately.
- Account for 553 exactly, because the arithmetic is the diagnosis and not a detail. One driver query, one desired-release lookup per row, one current-engagement lookup per row, and one reported-release lookup only for the rows where reported_release_id is non-null: 1 + 200 + 200 + 152. The 152 is why client-managed rows are cheaper, and if the sum did not land on the observed number something else is firing too.
- Sanity-check the cost model: 553 round trips at roughly 1 ms each is about 0.55 s on top of a 25 ms baseline, which accounts for the 640 ms. A hypothesis that does not reproduce the measured number is not finished.
- Batch it. Collect the ids from the driver query, then fetch releases with WHERE release_id = ANY($1) and the open engagement versions with WHERE engagement_id = ANY($1) AND effective_to IS NULL, and assemble in memory. Three statements per page, independent of page size.
- Check index usage before declaring victory. The engagement fetch must use the partial unique index on (engagement_id) WHERE effective_to IS NULL; without it, ANY($1) scans every amendment version of every engagement and the fix regresses harder than the bug did.
- Pin the behaviour with a test asserting a constant statement count per request, since no individual query here is slow enough to ever trip a slow-query alarm.
Follow-up
- The operator wants to sort the page by reported release version. Where does that sort have to happen now, and what does it cost?
- Page size goes to 1,000. Which breaks first, the driver query or the ANY($1) parameter list?
- reported_release_id NULL means unknown, not converged. How should the list render that, and what does the count at the top of the page then mean?
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Practice prompt ↗Practice prompt ↗Worked solution ↗02References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Practice prompt ↗Practice prompt ↗03Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Practice prompt ↗Practice prompt ↗04Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Practice prompt ↗Practice prompt ↗06Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Practice prompt ↗Practice prompt ↗07Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Team size, service count and tickets closed say very little. Seniority shows in the decision you owned: what you chose not to build, which constraint you traded away, whose objection you had to resolve before anything could move. A large project where you executed someone else's plan is a small story.
What is your experience with Power BI or other Microsoft Power Tools?
What is your experience with Power BI or other Microsoft Power Tools?
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
What would be your ideal career path or role?
What would be your ideal career path or role?
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?
If you are in a group project and your colleagues disagree with your p…
If you are in a group project and your colleagues disagree with your proposed solution, how do you convince them?
Approach
- 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.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
Can you explain your experience working with stakeholders?
Can you explain your experience working with stakeholders?
Approach
- Name the disagreement and how you resolved it with evidence.
- Give the blast radius: what could have broken, and what you measured.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
- 01
What is your experience with Power BI or other Microsoft Power Tools?
- 02
What would be your ideal career path or role?
- 03
If you are in a group project and your colleagues disagree with your proposed solution, how do you convince them?
- 04
Can you explain your experience working with stakeholders?
Is this an official RSM interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at RSM. Rounds and questions reflect what candidates have reported, not a process RSM has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗Is the technical assessment very difficult?
The difficulty varies, but expect it to be rigorous. It often includes data structures and algorithms, so consistent practice on core coding fundamentals is essential.
PracHub interview research ↗How much preparation time do I need?
Dedicate at least 2–3 weeks to review your resume, brush up on SQL and your primary programming language, and practice behavioral interview questions.
PracHub interview research ↗What is the company culture like?
RSM is professional and client-focused. You will be expected to work in a collaborative, team-oriented environment where clarity and precision are highly valued.
PracHub interview research ↗How long does the process take from start to finish?
The process typically involves several rounds, including technical exams and multiple interviews with management. It can take several weeks, so remain patient and communicative with your recruiter.
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