As a Software Engineer at CAP Digisoft Solutions, you are positioned at the intersection of creative design and technical implementation. This role is critical to the firm’s ability to deliver high-quality web and graphical solutions that meet client expectations and drive business growth. You will be responsible for translating complex design requirements into functional, efficient code, ensuring that the end-user experience is both visually compelling and technically sound.
This position offers the opportunity to work on diverse projects that require a blend of front-end proficiency and solid architectural thinking. You will contribute to the development lifecycle by maintaining clean code standards, optimizing performance, and collaborating with cross-functional teams to solve technical challenges. Success in this role requires a balance of technical rigor and a proactive approach to problem-solving, as you will often be tasked with delivering solutions that are both innovative and scalable.
While the role is titled Software Engineer, be prepared to demonstrate proficiency in both development logic and design-centric technologies like CSS and Bootstrap, as the work often overlaps with web design requirements.
Preliminary Screening
reportedYou cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.
What to demonstrate
- Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
- Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
- Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
- Whether your part in a team effort is stated accurately, including what other people did
How to prepare
- Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
- Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
- Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.
Hands-on Technical Validation
reportedWhat this round decides is narrow: whether you can produce code that runs and is correct on inputs nobody showed you. An elegant solution that does not compile scores below a plain one that does, so write a correct brute force first, say out loud that you know its cost, and improve it with the working version still on screen. What separates strong answers is who finds the broken case. Trace your own code against an empty input, a single element, and duplicate keys before you say you are finished, because being told is far more expensive than noticing.
What to demonstrate
- Whether degenerate inputs get checked without being asked for: an empty collection, one element, every element equal, and the extreme value the input type allows
- Whether the complexity you state matches the code you actually wrote, including a sort or a copy sitting inside a loop
- Whether the finished answer is verified against the worked examples before you call it done, rather than assumed correct because the code reads correctly
How to prepare
- Take five problems you have already solved and, without running anything, write down what each returns for empty input, a single element, and all-duplicates. Then run them and count how many you predicted wrong.
- Drill the brute force as its own skill: on ten problems, write only the obviously-correct slow version and time how long it takes to get it passing. If that is more than a few minutes, that is what to practise, not the optimal version.
- Add a fixed last step before you submit anything, reading only the loop bounds and the initial value of each accumulator, which is where most off-by-one errors live
Machine Test
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.
Long-lived shared credentials for client systems
One static key used by the connector runtime, a debugging script and three engineers cannot be attributed to a person in an audit, cannot be expired at engagement close, and cannot be rotated without breaking an unknown number of consumers at once. The expensive part of the eventual incident is not the leak; it is that rotation requires finding every holder under time pressure, and nothing recorded who they were. Issue per-principal, per-target, short-lived grants from a broker so rotation is a no-op, attribution is automatic, and closure is a TTL rather than a search.
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.
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.
Not asking what the system looks like if it dies halfway through
For any multi-step write, say what state remains if the process stops between step two and step three, and what brings it back: a single transaction, a saga with compensating actions, an outbox, or a reconciliation job. Partial failure is routine at any real call volume, so 'that shouldn't happen' is an answer with nothing behind it.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Resolve effective-dated engagement versions for two million access checks
The engagement table holds 500,000 effective-dated rows: (engagement_id, version, effective_from, effective_to which is NULL for the open version). A database exclusion constraint guarantees that per engagement_id the tstzrange(effective_from, effective_to) values never overlap, and the ranges are half-open. You must answer 2,000,000 access checks of the form (engagement_id, at_time) in one process with no database, returning the version open at that instant or none. Also report, per engagement, any coverage gap between adjacent versions. Give the complexity of the scan-per-check approach and say why it is not an option.
Approach
- State the naive cost first: scanning the table per check is 2e6 x 5e5 = 1e12 range comparisons. Even at 1e9 simple comparisons per second that is roughly fifteen minutes of pure compare work, against a budget measured in seconds. It is correct, just unaffordable, and the fix is an index built once rather than a faster comparison.
- Build a hash map engagement_id -> array of versions sorted by effective_from, once. O(V log V) to sort, O(V) memory. The build is amortised over 2e6 lookups, which is what makes it worth doing at all.
- Answer each check by binary-searching for the last effective_from <= at_time, then confirming effective_to IS NULL OR at_time < effective_to. If the confirmation fails, the instant lies in a gap and the answer is none, not the neighbouring version. O(log v) per check, O(V log V + Q log v) total.
- Name the precondition: predecessor search is only valid because the ranges per engagement do not overlap, which the exclusion constraint enforces with btree_gist for the equality operator. Without that guarantee you need an interval tree returning all stabbed intervals, at O(log n + k). Use the same half-open convention as the database, where at_time equal to effective_to belongs to the next version.
- Find gaps with one O(V) pass over each sorted array comparing prev.effective_to against next.effective_from. The exclusion constraint forbids overlaps, not holes, so a hole is entirely possible and is where an access check fails closed for a reason nobody can see in the contract.
- If the checks can be buffered, sort them by (engagement_id, at_time) and merge against the version arrays: O(Q log Q + V log V), usually a large constant-factor win over 2e6 random binary searches because the merge walks memory in order.
Worked solution 35 min
- Index engagement E1 with v1 [2026-01-01, 2026-04-01), v2 [2026-04-01, 2026-07-01), v3 [2026-07-01, NULL), and engagement E2 with v1 [2026-01-01, 2026-03-01) and v2 [2026-04-01, NULL).
- Query (E1, 2025-12-31): predecessor search finds no effective_from <= at_time, so the answer is none.
- Query (E1, 2026-04-01T00:00:00Z): the predecessor is v2, not v1, because the boundary instant belongs to the later range under half-open semantics. Query (E1, 2026-06-30T23:59:59Z) returns v2 and (E1, 2026-09-01) returns v3.
- Query (E2, 2026-03-15): the predecessor is v1, but at_time is not less than v1.effective_to (2026-03-01), so the confirmation fails and the answer is none.
- Adjacent-pair pass on E2: v1.effective_to = 2026-03-01 is earlier than v2.effective_from = 2026-04-01, so report the gap [2026-03-01, 2026-04-01).
Follow-up
- The open version has effective_to NULL. How do you keep both the sort and the comparison total without a sentinel that overflows your timestamp type?
- A gap exists for one engagement and a check lands in it. What should the access path return, and what should it emit so someone fixes the amendment?
- Amendments are being written while you answer checks. What read model keeps an answer self-consistent, and what does a check mean if the snapshot is a second stale?
Assign connector bindings to rollout waves and name the cycle
Connector bindings declare ordering dependencies: an edge u -> v means v must not run in a wave earlier than one after u has succeeded. You have up to 50,000 bindings and 200,000 edges, and a misconfigured dependency may have created a cycle. Assign each binding the earliest wave number such that every predecessor sits in a strictly earlier wave, return the total wave count, and if a cycle exists return the bindings on one concrete cycle rather than a boolean. The implementation must be iterative: a dependency chain can be 50,000 deep.
Approach
- Build an adjacency list and an indegree array, then run Kahn's algorithm from every indegree-zero node. Bindings with no predecessors get wave 0. O(V+E) time, O(V+E) space.
- Relax waves as you decrement: when popping u, set wave[v] = max(wave[v], wave[u]+1) for each successor before enqueueing v at indegree zero. Because v is only enqueued after its last predecessor is popped, wave[v] is 1 + max over predecessors, which is the earliest legal wave. Total wave count is max(wave)+1, the longest path in nodes.
- If Kahn emits fewer than V nodes, the residual subgraph is exactly the nodes lying on or downstream of a cycle. Every residual node still has an in-edge inside the residual, otherwise it would have been popped, so walking backwards along in-edges cannot dead-end and must revisit a node within |residual| steps; the segment between the two visits is a concrete cycle. O(V) with a visited-position map.
- Use an explicit stack or a queue rather than recursion. Recursive DFS on a 50,000-node chain exceeds the default stack in most runtimes, and the failure looks like a crash rather than a cycle.
- At this size, recompute the whole plan whenever an edge changes. Incremental topological order maintenance costs far more code than the O(V+E) rebuild it saves.
Follow-up
- Two bindings land in the same wave but hit the same client endpoint, whose rate budget is shared. How does the wave plan have to change, and what problem class does that turn into?
- One dependency holds only for overlapping watermark ranges rather than globally. Is the dependency graph still a DAG over bindings, and what is the right vertex if not?
- Report the critical path so an operator can see which chain sets the wave count, not just that it is five.
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?
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.
Worked solution 25 min
- Write the snapshot record's field list and key, marking each field as write-invalidated or time-evaluated.
- Write the publisher query that resolves the open engagement version and aggregates scopes per (client_id, principal_id).
- Write the consumer's decision function, taking the snapshot plus a current timestamp, and show that it denies on missing, on stale, and on ends_on plus grace elapsed.
- Write the reconciliation query that recomputes a sample of keys from source and diffs them against the published snapshot.
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?
Explain why the failed-run backlog query ignores three indexes
connector_run holds roughly six million rows over three years. Its indexes are (connector_id, started_at DESC); (started_at) WHERE status = 'failed'; and ((finished_at AT TIME ZONE 'UTC')::date). The operator backlog query is SELECT run_id, connector_id, failure_class, started_at FROM connector_run WHERE environment_id = $1 AND status = ANY($2) AND started_at >= now() - interval '7 days' ORDER BY started_at DESC LIMIT 50, with $2 bound to {failed, ambiguous}. It sequentially scans. Explain index by index why each is unusable, then give the one index you would add.
Approach
- Index one leads on connector_id, which this query never constrains. A B-tree can only be probed from a prefix of its key columns, so with no equality on connector_id it degenerates into a full index scan plus heap fetches, which the planner will rightly cost above a sequential scan. PostgreSQL 18 added B-tree skip scan for an unconstrained prefix, so state your server version before assuming this is fatal; it helps only when that prefix has few distinct values, and hundreds of connectors is already too many to rely on it.
- Index two is partial on status = 'failed', and the rows the query wants are partly not in it at all. Predicate implication runs one way: the query's predicate must imply the index's. status IN ('failed','ambiguous') is weaker than status = 'failed' and implies nothing, so no amount of inlining the parameter helps. It would serve only the failed branch of a UNION ALL.
- Index three is an expression index on a function of finished_at, while the query filters started_at as a plain range. Even on the right column, a range predicate on the bare column cannot use an index on a function of it. Note also why the AT TIME ZONE 'UTC' wrapper is present: finished_at::date on a timestamptz is STABLE rather than IMMUTABLE because it depends on the session TimeZone, so PostgreSQL refuses to index it without a fixed zone.
- Add (environment_id, started_at DESC) WHERE status IN ('failed','ambiguous'): equality on the scoping column first so the scan starts in the right place, the ordering column second so ORDER BY started_at DESC LIMIT 50 stops after fifty index entries instead of sorting the week, and partial on the status set so the index covers the small failing minority rather than six million rows.
- Confirm the partial predicate is provable for the query's parameterised form. Under a custom plan the array parameter is substituted as a constant and the prover handles the ScalarArrayOpExpr; under a generic plan it stays a Param and the proof fails, so check plan_cache_mode or the sixth execution of a prepared statement before declaring victory.
- Say what you deliberately did not do. Adding INCLUDE (run_id, connector_id, failure_class) to chase an index-only scan pays only while the visibility map is current, which ties the query's latency to whether autovacuum is keeping up with the write rate — a dependency worth accepting consciously, not by accident.
Follow-up
- The same query is also run without the environment filter, for an estate-wide backlog. Does your index still help, and what would you add instead?
- How does row-level security on this table interact with your index, given that policy quals are applied before non-leakproof user quals?
- Six months from now the partial index covers 30 percent of the table. What signal tells you to change it?
How would you implement inheritance and polymorphism using PHP?
How would you implement inheritance and polymorphism using PHP?
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
- What assumption would you test first?
- How would you know your answer was wrong?
Can you explain the structure and logic behind your final year project…
Can you explain the structure and logic behind your final year project?
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
- What assumption would you test first?
- How would you know your answer was wrong?
What are the core principles of Object-Oriented Programming (OOP)?
What are the core principles of Object-Oriented Programming (OOP)?
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?
How do you implement responsive design using CSS and Bootstrap?
How do you implement responsive design using CSS and Bootstrap?
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Design the connector work queue under skewed client quotas
Hundreds of connector bindings run a few thousand jobs a day across a worker fleet. Each binding targets one client endpoint whose quota, say 600 requests a minute, is shared by every job you point at that client, including operator-triggered ones. Volume is skewed: roughly 5% of bindings produce most of the traffic and most of the failures. Design the queueing and admission control. State the key that concurrency and rate limiting are enforced on, what is still admitted while one endpoint returns 429 for an hour, what gets merged or dropped as the backlog grows, and how a small binding still runs on time.
Approach
- Key the limiter on (client_id, endpoint), never on the worker or the job. The quota belongs to the client and is spent by every process you aim at them, so a per-worker limit silently multiplies by fleet size and a single global limit starves everyone the moment one client is hot. Implement it as a distributed token bucket in Redis behind one Lua script so the check and the decrement are atomic, with the refill set below the client's real limit, measured rather than read from their documentation.
- Give every key its own queue and lease a bounded number of in-flight runs per key. One shared FIFO puts a backed-up large binding in front of a small one - head-of-line blocking, and the actual complaint you will receive. Workers claim with SELECT ... FOR UPDATE SKIP LOCKED over due runs and check the per-key in-flight count before claiming.
- Treat 429 as a control signal rather than an error: honour Retry-After when present, otherwise exponential backoff with full jitter, sleep = random(0, min(cap, base * 2^attempt)). Deterministic backoff resynchronises every job against the same client and rebuilds the burst you just backed away from. While a key is in backoff, stop admitting its runs rather than admitting and failing them, so your queue absorbs the pressure instead of the client's endpoint.
- Bound the queue instead of letting it grow. By Little's law a key admitted at a rate above its service rate grows without limit, so decide what is dropped before the backlog decides for you: scheduled incremental runs are idempotent and coalescable, so two pending runs over overlapping cursor ranges collapse into one wider range, while an operator-triggered run is not coalescable and needs a small reserved lane.
- Name the trade-off. Per-key isolation costs fleet utilisation, because workers idle while a hot key is throttled, and buys predictable latency for the long tail of small bindings. Measure per-key scheduling delay percentiles, not fleet throughput - throughput is dominated by the noisy 5% and hides what the other 95% experience.
Worked solution 30 min
- Model two bindings on one client key, one issuing 500 requests a minute and one issuing 5, under a shared 600 a minute budget, served by a single FIFO queue.
- Measure the small binding's scheduling delay, then rerun with per-key queues plus a reserved lane and measure again.
- Inject an hour of 429 responses with deterministic backoff across 20 workers and plot the request rate at each retry boundary.
- Repeat with full jitter and compare the peak rate and the time to drain.
Follow-up
- The client's quota turns out to be per credential rather than per organisation, and you hold three. Does the key change, and does anything else?
- A run has sat queued for six hours behind a throttled binding. What does the client-facing status say during those six hours, and when do you abandon it?
- How do you establish the real limit without tripping it inside the client's production system?
Reports intermittently return another client's rows under load
A shared-pool deployment scopes every query with row-level security: the application runs SET app.client_id at the start of each request, and policies compare client_id against current_setting('app.client_id'). Twice this week a client's report contained rows belonging to a different client. Staging cannot reproduce it, single-request replays are clean, and the SQL text is identical in the good and bad cases. Connections come from a pool shared by all requests. Give the ordered diagnosis, the reproduction, and the fix.
Approach
- Order the hypotheses by what the evidence already excludes. Identical SQL text and clean single-request replays rule out a missing predicate in that endpoint, so the variable is not the query, it is the value the policy reads when the query runs. Check, in this order: is RLS enabled and forced on the table; is the serving role actually subject to it; and only then, what does current_setting return at execution time.
- Confirm the policy is in force before blaming it. Policies do not apply to the table owner unless FORCE ROW LEVEL SECURITY is set, and never apply to a role with BYPASSRLS or to a superuser. If traffic is served by the owning role, the policy has been decorative all along and the leak is not intermittent at all, only intermittently noticed.
- Instrument the value, not the code. As the first statement inside every transaction, read current_setting('app.client_id', true) and compare it with the tenant on the request, emitting a counter on mismatch. A defect invisible to replays shows up here within minutes of real concurrent traffic.
- Explain why pooling makes it intermittent. A plain SET is session-scoped and survives COMMIT, so the backend returns to the pool carrying the previous request's value. This needs no transaction-pooling proxy; an ordinary application connection pool is sufficient. The next checkout that sets the value late, or skips it on some path, inherits the previous tenant, and row-level security then filters flawlessly for the wrong client.
- Reproduce deterministically rather than by load-testing and hoping. Cap the pool at one connection, issue two requests for different tenants back to back, and have the second issue its first query before its SET. Zero rows would be the fail-closed outcome; the other tenant's rows is the leak, and the difference between those two results is exactly whether the previous value was left behind.
- Fix in layers. Use set_config('app.client_id', $1, true) or SET LOCAL inside the transaction so the value dies at COMMIT; assert it matches the request immediately before the first query rather than trusting an upstream middleware; set FORCE ROW LEVEL SECURITY; and serve traffic with a role that neither owns the tables nor holds BYPASSRLS, reserving the owning role for migrations.
Follow-up
- You owe a breach notification within contractual time. What query bounds which client pairs and which rows were exposed, and what in the schema makes that answerable at all?
- A background job has no inbound request and therefore no tenant. Where does its tenant come from, and what stops it from being where this happens next?
- How does the same failure mode look on the dedicated and client-managed tiers, and what does that tell you about where isolation should live?
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 ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.
Disagree in review about a dedupe key derivation
A colleague's connector change computes its dedupe key as a hash over the whole source payload. You believe it must be derived from named, stable source fields with a stored derivation version, because the client renames and retypes fields without notice and every previously computed key must stay reproducible. Recount a code review disagreement of this shape that you actually had. Give the argument you made, the evidence that moved it, how long it stayed open, and what you did when the author was still unconvinced.
Approach
- State the technical claim as a failure rather than a preference: hashing the whole payload means any upstream field addition changes every key, so the next run finds no ledger hit and re-applies records the client already has.
- Say what evidence you produced. A replay of a real past payload change through both derivations, or a count of fields in the sample that have already changed shape, wins a review; seniority does not.
- Describe the boundary you used between disagreeing and committing: reversible choices get one round and then the author decides, while a choice that corrupts persisted data is worth escalating. Say which this was and why.
- Include the cost you accepted. A versioned derivation means storing the input field names and the derivation version alongside the hash, and keeping each old version's code path alive as long as its keys can still be looked up.
- Close with what the review changed beyond the one diff: a test that pins the key across a renamed field, or a written rule about what may feed a dedupe key.
Follow-up
- The client renames a field that feeds the key. Walk through what your derivation version does on the next run, and what happens to records already applied under the old key.
- The author was senior to you and unconvinced, and the merge was needed that afternoon. What did you do?
Ship a connector on a deadline with named debt
Describe shipping something you knew was incomplete against a fixed external date: a client change window, a month-end, a contractual go-live. Name the specific shortcut, such as an ambiguous outcome classified as a plain failure, a watermark advanced outside the ledger transaction, or an unchunked backfill. State the bound you put around it, who accepted the risk and where that was recorded, what would have made you refuse to ship, and what actually happened to the debt afterwards. Do not describe debt you never wrote down.
Approach
- Name the shortcut precisely enough that its failure mode follows from the description: no ambiguous state, so a timeout retried and duplicated; or the watermark committed in a different transaction from the ledger row, so a crash between them loses or replays a range.
- Give the bound, which is what separates a judged risk from a gamble: a first run restricted to a reversible slice, a record cap, a kill switch, or a reconciliation read against the target scheduled before the next run.
- Say who accepted the risk and in what artefact. 'The client knew' is credible only with a written record and a date.
- State your refusal line and why this fell on the shipping side of it. Irreversibility is the usual test: damage you can undo ships, silent corruption inside a client's system does not.
- Report what happened to the debt with a date, including the case where it is still live, and what it has cost since.
Follow-up
- The shortcut fires in production. Walk through the first hour: what you look at, in what order, and what you tell the client.
- Same deadline, but the shortcut risks writing duplicates into the client's system rather than into yours. Does your answer change?
Own a cross-tenant read that reached a client
Prepare a five-minute account of an isolation or data-exposure incident you owned: a query that returned another tenant's rows, a cache keyed without a tenant, or a report that crossed a boundary. State the mechanism precisely, how you established blast radius (which tenants read which tenants' rows, over what window), the containment step, and the fix that made the class impossible rather than the instance. Finish with the notification decision and who made it. If you have never owned one, use the closest near-miss and say so.
Approach
- Open with the mechanism in one sentence rather than the symptom. The canonical version here: a session-scoped SET app.tenant_id on a connection returned to a transaction-mode pool with that value still attached, so the next checkout inherited it and row-level security then enforced the previous tenant's policy flawlessly.
- Separate containment from fix. Containment is what you did in the first twenty minutes (drain the pool, switch the pool to session mode, disable the endpoint); the fix is structural (SET LOCAL, which dies with the transaction, plus an assertion that the setting equals the request's tenant immediately before the first statement).
- Give the blast-radius method, not an adjective: reconstruct from the query log joined to request context on a correlation id, count distinct (reading tenant, row tenant) pairs and rows, and state what you could not reconstruct and why.
- Name the class-level fix and its cost. FORCE ROW LEVEL SECURITY so the table owner is not exempt, separate migration and application roles because a role with BYPASSRLS defeats every policy, and a test that drives two tenants' requests concurrently over one pooled connection, which is the load profile a serial integration suite never produces.
- Close with the notification call: it is a contractual question, not only an engineering one, so say who decided, how long the decision took, and what you would not repeat.
Follow-up
- Your suite ran one request at a time and passed. What test would have caught this, and what does it cost to run on every change?
- The same defect inside a dedicated deployment touches one client. Does that change your severity, your containment, or only your disclosure?
- How do you know today that no other endpoint in the estate has the same defect?
- 01
A colleague's connector change computes its dedupe key as a hash over the whole source payload. You believe it must be derived from named, stable source fields with a stored derivation version, because the client renames and retypes fields without notice and every previously computed key must stay reproducible. Recount a code review disagreement of this shape that you actually had. Give the argument you made, the evidence that moved it, how long it stayed open, and what you did when the author was still unconvinced.
- 02
Describe shipping something you knew was incomplete against a fixed external date: a client change window, a month-end, a contractual go-live. Name the specific shortcut, such as an ambiguous outcome classified as a plain failure, a watermark advanced outside the ledger transaction, or an unchunked backfill. State the bound you put around it, who accepted the risk and where that was recorded, what would have made you refuse to ship, and what actually happened to the debt afterwards. Do not describe debt you never wrote down.
- 03
Prepare a five-minute account of an isolation or data-exposure incident you owned: a query that returned another tenant's rows, a cache keyed without a tenant, or a report that crossed a boundary. State the mechanism precisely, how you established blast radius (which tenants read which tenants' rows, over what window), the containment step, and the fix that made the class impossible rather than the instance. Finish with the notification decision and who made it. If you have never owned one, use the closest near-miss and say so.
Is this an official CAP Digisoft Solutions interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at CAP Digisoft Solutions. Rounds and questions reflect what candidates have reported, not a process CAP Digisoft Solutions has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long does the interview process typically take?
The process usually involves 2–3 rounds held at the office. Candidates should be prepared for a potential waiting period after the final round as the team concludes their evaluation.
PracHub interview research ↗What is the most important part of the interview?
The machine test is arguably the most critical component. Ensure your code is clean, efficient, and that you are prepared to explain the logic behind your implementation.
PracHub interview research ↗How should I prepare for the HR rounds?
Be ready to discuss your background, your interest in the company, and your technical projects. Practice providing clear, concise answers to avoid being interrupted or redirected.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-22 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-22