Interview guides · Apptegy
Apptegy Software Engineer Interview Questions & Guide 2026
Code reading practice for Apptegy software engineer prep: work out what unfamiliar code does before judging it.
The 60-second brief
A Software Engineer at Apptegy plays a pivotal role in building and scaling the platforms that help school districts communicate with their communities. You are not just writing code; you are contributing to a product ecosystem designed to bridge the gap between technology and the educational experience. Your work directly impacts how schools share their stories and manage their digital presence, requiring a balance of technical precision and a deep understanding of user needs.
If the seat owns a service boundary, scope your preparation toward failure behaviour rather than topology. Retrying over an at-least-once channel produces duplicates by construction, so a retry policy is only as safe as the idempotency key underneath it.
Focus areas
- Absorb load that arrives on bell boundaries
- Make outbound grade publication idempotent and ordered
- Scope every query, cache and job by tenant
Overview
This role is both challenging and rewarding because it requires you to operate within a fast-paced environment that values intentionality and cultural alignment. You will collaborate with cross-functional teams to tackle complex problems, ensuring that the software remains robust, intuitive, and effective. Whether you are working on the full stack or focused on specific platform components, your contributions are expected to drive the company’s mission forward while maintaining high standards for software craftsmanship.
Apptegy places a high value on cultural fit. While technical proficiency is necessary, your ability to communicate your passion and align with their collaborative, mission-driven environment is equally critical to your success.
Where to focus your preparation
Treat the sections below as preparation areas and confirm the format with your recruiter.
Preparation focus
No round sequence has been reported for this company, so work the categories below and confirm the format with your recruiter.
What to demonstrate
- Breadth across SQL, experimentation and product reasoning
- Ability to state assumptions before choosing a method
How to prepare
- Drill the practice exercises below and time yourself
- Prepare three quantified stories about decisions you drove
Where candidates lose points
Treating the launch subject identifier as a global user id, or merging identities on email.
The subject claim is stable only within its issuer, so the same human arriving from a second platform is a different subject and must be a second external identity bound to the same person. Email is worse than useless as a merge key here: privacy settings frequently suppress it from the launch entirely, districts recycle addresses between graduating and incoming students, and younger learners often have none. A merge on email eventually joins two children's work into one account, which is both a grade bug and a privacy incident.
Running learner submissions without hard, enforced resource ceilings.
Submitted code is untrusted input written by people who are actively learning and sometimes actively probing. Without a wall-clock limit alongside the CPU limit, a sleeping or blocked process pins a worker; without a process/thread cap, a three-line fork bomb takes the host; without an output cap, a stray print loop fills the disk and the log pipeline; without network isolation, a submission can reach internal services or exfiltrate the hidden tests. Each ceiling has to be enforced by the kernel or the runtime, since a limit checked by the harness after the fact is not a limit.
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.
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.
Questions & practice
Dispatch a bell-driven autograde burst under per-tenant caps
Hard · scheduling · heap of queues · fairness · np-hard
Thirty thousand autograde jobs enqueue over 90 seconds as a district's periods end. Each carries tenant_id, enqueued_at, a language-dependent wall_limit_seconds between 2 and 15 with a mean near 3, and a deadline equal to its section's period end. You have 400 one-shot workers, jobs cannot be preempted once started, and no tenant may hold more than 20 percent of the workers at once. Choose the dispatch order that maximises jobs returning feedback before their deadline, give the data structure and its per-dispatch cost, and say plainly whether that order is optimal.
Approach
- Do the capacity arithmetic first, because it reframes the question. Four hundred workers at a 3-second mean serve about 133 jobs per second; 30,000 arrivals over 90 seconds is 333 per second. Twelve thousand are served during the burst, leaving 18,000 queued, which drains in about 135 more seconds. The system is oversubscribed by roughly two and a half times, so the scheduler is not deciding whether anyone misses a deadline — it is deciding who.
- Choose earliest-deadline-first and then state its standing precisely. On a single machine with preemption, EDF meets every deadline whenever any schedule can. This is neither: 400 machines and non-preemptive jobs of unequal length. Minimising the number of late jobs on parallel identical machines is NP-hard — even minimising makespan on two machines reduces from PARTITION — so EDF here is a defensible heuristic, not an optimum. On one machine, Moore-Hodgson would give the exact optimum in O(n log n); that exactness does not survive the move to 400.
- Enforce the cap with a heap of queues rather than by scanning. Keep one min-heap per tenant ordered by deadline, and a global min-heap holding at most one entry per eligible tenant keyed on that tenant's head deadline. Dispatch pops the global head, pops that tenant's head job, increments its in-flight count, and re-pushes the tenant only if it is still under 80 workers. On completion, decrement and re-insert the tenant if it was parked with work waiting. Each dispatch is O(log T + log J_t) instead of O(J), and the cap costs a counter comparison rather than a rescan.
- Add admission control, which is where most of the win actually is. A job whose
now + wall_limit_secondsalready exceeds its deadline cannot return feedback in time no matter what, so running it burns a worker slot that a still-feasible job could have used. Route it to a graded-after-the-bell path immediately. This is the Moore-Hodgson instinct — discard the jobs you cannot save — applied as a filter rather than as an exact algorithm. - Break ties on
enqueued_atascending so the order is deterministic and a job cannot be starved by a stream of equal-deadline arrivals. Without a deterministic tiebreak the same burst replays differently, and an operator comparing two incidents cannot tell a scheduling change from noise. - Give the honest ceiling. No dispatch order makes a 2.5x oversubscription feasible; the levers are worker count, wall limits per language, and whether feedback is required before the bell at all. A candidate who presents EDF as the fix, rather than as the allocation of an unavoidable shortfall, has answered a different question.
Follow-up
- One tenant submits 20,000 of the 30,000 jobs. Show what your cap does to the other tenants' deadline-miss rate versus pure EDF.
- Workers become preemptible with a checkpoint. Does EDF's optimality argument come back, and under exactly what conditions?
- A job exceeds its wall limit and is killed. Does it re-enter the queue, and what does your answer do to the deadline guarantees of everything behind it?
Diff a roster snapshot against live enrollments in one pass
Easy · hash join · diffing · soft delete
A tenant's bulk roster snapshot has been normalised into N incoming enrollment records carrying section_id, person_id, role (learner, instructor, aide, observer), source, begin_date and end_date. You also hold M stored enrollment rows for that tenant where deleted_at IS NULL. N and M are each up to 1,000,000. In one pass over the incoming records, classify each as an add, an update or a no-op, and produce the set of stored rows the snapshot never asserts, which becomes deletes_proposed on roster_sync_run. State your time and space bounds in bytes.
Approach
- Fix the key first, because the schema already decided it: the partial unique index is
(tenant_id, section_id, person_id, role) WHERE deleted_at IS NULL, soroleis part of the identity. One human legitimately holds two live rows in a section (instructor and observer), and keying on(section_id, person_id)alone collapses them into one, emitting a spurious update and a spurious delete on every run. - Load the M stored rows into an open-addressed hash map from that 3-tuple to
(enrollment_id, comparable_fields, seen_flag). Packed, the key is 24 bytes and the payload about 8, so at a 0.7 load factor that is roughly 45-50 MB for a million rows; boxed objects in a JVM or CPython map cost 5-10x that, which is the number that decides whether this fits in a worker. - Stream the incoming records and probe once each: miss is an add, hit with equal comparable fields is a no-op, hit with any difference is an update. Set the seen flag on hit. After the stream, the stored entries still unseen are
deletes_proposed; count them and divide by M to get the fraction the gate is evaluated on. - Handle revival explicitly: an incoming record that misses the live map may still match a soft-deleted row. Look that up and clear
deleted_atin place rather than inserting a newenrollment_id, because the partial index constrains only live rows and will not catch the duplicate — the history simply fragments into a new id at every term boundary. - Complexity is O(N + M) expected time and O(M) space. State the alternative and its crossover: sorting both sides and merging is O(N log N + M log M) with O(1) working memory beyond the sort, and it wins as soon as M stops fitting in RAM, since an external sort degrades gracefully where a hash map does not.
- Guard the semantics before any of this runs. Absence means deletion only for a bulk feed; on a delta feed absence is silence and only an explicit
tobedeletedstatus removes anything. A tenant that switches feed modes mid-term will otherwise have one delta file read as a snapshot, which proposes deleting almost everything.
Worked solution (25 min)
- Write the key tuple down before any code and check it against the partial unique index in the schema, not against intuition about what a class roster is.
- Build the stored map with a
seenbit per entry, then run the incoming stream through a single probe per record, appending to three lists: adds, updates, no-ops. - Sweep the map once at the end for unseen entries to get
deletes_proposed, and compute the fraction against the live count captured before the run started. - Feed a fixture with a person holding learner and observer rows in one section, a row whose
end_datealone changed, a previously soft-deleted row that the snapshot asserts again, and one genuinely absent row. - Re-run the same fixture end to end and confirm every count except
rows_inis zero the second time.
Expected result: One update for the changed end_date, one revival that keeps the original enrollment_id, one proposed delete, and no rows at all for the dual-role person; the second run reports zero adds, zero updates and zero proposed deletes.
Verify it yourself
- adds + updates + no_ops equals N exactly, with no record falling through unclassified.
- After the apply, a live-row count query returns M + adds - deletes_applied.
- Shuffling the incoming file changes no counts, since a hash diff must be order-independent.
- Removing
rolefrom the key makes the dual-role fixture report one update and one delete, which is the regression test for this bug.
Follow-up
- The upstream system changes its
external_sourced_idscheme, so every incoming row looks new and every stored row looks absent. How do you reconcile without creating duplicate persons? - The apply aborts halfway through. What makes the next run finish rather than double-apply, and what does
snapshot_digestdo for you? - M grows to 50 million for one tenant. Which of your two algorithms do you run, and what changes about the delete detection?
Backfill frozen lateness without converting a hundred million times
Hard · cardinality reduction · time zones · batched backfill
A backfill must set due_at_utc on 1,000,000 assignment rows where it is NULL, from due_at_local plus policy_time_zone, and then set is_late on the 100,000,000 attempt rows beneath them whose is_late is NULL, by comparing submitted_at to that instant. Converting one local time per attempt row is correct and far too slow. Give a plan that is both faster and reproducible when re-run months later, state the speed-up you expect, and define the result for a local due time that is ambiguous or nonexistent across a daylight-saving transition.
Approach
- Name why the naive plan is slow rather than asserting it. A local-to-UTC conversion is a binary search over that zone's transition table plus an object construction, on the order of one to three microseconds in a managed runtime; 100,000,000 of them is a few minutes of pure conversion and, once per-row fetch and allocation are included, hours. Nothing about it is wrong — it is the row count multiplied by a cost that did not have to be paid per row.
- Collapse the cardinality. The conversion depends only on
(policy_time_zone, due_at_local), and due dates cluster hard on end-of-day instants for school days, so the distinct pair count is on the order of 10,000 against 100,000,000 attempts. Convert the distinct pairs once into a small mapping, then resolve each attempt with a hash probe. That is roughly a 10,000-fold reduction in conversions, and the per-row cost drops from a conversion to a lookup. - Do it in two stages so the second one is a join, not a computation: materialise
due_at_utcon the 1,000,000 assignment rows from the mapping, then setis_lateon attempts by joining onassignment_id. The attempt stage then performs no time-zone work at all, which is also what makes it re-runnable without the tz database on the path. - Pin reproducibility explicitly. The local-to-UTC mapping is a function of the zone, the local time, and the time-zone database version, because releases correct historical transitions. Record the tzdata version on the backfill run, or a re-run months later can produce a different instant for the same row and a grade will move with no grade event behind it.
- Define the two degenerate cases instead of letting a library default decide. An ambiguous local time in a repeated hour maps to two instants; a nonexistent one in a skipped hour maps to none. Pick the rule — the later instant when ambiguous, the first instant after the gap when nonexistent — encode it as an explicit flag rather than relying on a fold default, and assert both in tests. An 11:59 PM due time is outside the affected hour for most zones, so this is rare, which is exactly why it will not be noticed when it is wrong.
- Respect the freeze. Update only rows where
is_late IS NULL; a row that already carries the flag was decided at submit and must not be recomputed, or the backfill becomes a retroactive regrade. Then chunk the update by primary-key range with a bounded transaction size, because one 100,000,000-row statement holds locks and generates dead tuples for the whole run and cannot be resumed after a failure.
Follow-up
- A district is moved to a different school with a different time zone mid-term. Which rows, if any, change, and what does the freeze rule say?
- The tzdata version on the backfill host differs from the one in the application image. How would you have found that out before the run rather than after?
late_policyisblockfor some assignments, so a late submission should not exist. What do you write for an attempt that submitted after the due instant under that policy?
Answer a point-in-time roster question against soft-deleted enrollments
Easy · soft deletes · audit history · exclusion constraints
enrollment(enrollment_id, tenant_id, section_id, person_id, role CHECK IN ('learner','instructor','aide','observer'), source, begin_date, end_date NULL, status, created_at, updated_at, deleted_at NULL) carries partial UNIQUE (tenant_id, section_id, person_id, role) WHERE deleted_at IS NULL. A learner withdraws in week 3 and re-enrols in week 6; one person is both aide and observer in the same section. Write the query returning learners enrolled in one section on a given date, name the index that serves it, and say what the deleted rows can and cannot tell you about a past roster.
Approach
- Separate the two time axes before writing anything. begin_date and end_date are valid time — when the enrollment was true in the world. deleted_at is transaction time — when the system stopped asserting the row. An 'as of date D' roster is a valid-time question, so the date predicate does the work and deleted_at only filters what you currently believe.
- Write it as: SELECT p.person_id, p.display_name FROM enrollment e JOIN person p ON p.person_id = e.person_id AND p.tenant_id = e.tenant_id WHERE e.tenant_id = $1 AND e.section_id = $2 AND e.role = 'learner' AND e.deleted_at IS NULL AND e.begin_date <= $3 AND (e.end_date IS NULL OR e.end_date >= $3) ORDER BY p.display_name. The role predicate, not DISTINCT, is what stops the dual-role person appearing twice.
- Notice what the partial unique index forbids: two live rows for the same (tenant, section, person, role) even when their date ranges are disjoint, so a genuine withdraw-and-return cannot be modelled as two live intervals. Either the first row is soft-deleted, which destroys the interval, or the uniqueness moves to EXCLUDE USING gist (tenant_id WITH =, section_id WITH =, person_id WITH =, role WITH =, daterange(begin_date, end_date, '[]') WITH &&) WHERE (deleted_at IS NULL), which needs the btree_gist extension and permits disjoint intervals while still rejecting overlaps.
- Index for the equality prefix: (tenant_id, section_id, role, begin_date) WHERE deleted_at IS NULL. Only the leading equality columns plus the first inequality are scan keys, so end_date belongs in the index for the visibility of the filter, not as a second range key.
- State the limit of a soft delete: it stores one bit and one timestamp, so it answers 'is this row still asserted', not 'what did we believe on D'. Reconstructing a past belief needs an append-only history row per change, or the recorded diff from the ingestion run; if support has to answer why a learner vanished, the run record is the artefact, not deleted_at.
Follow-up
- The feed sends two overlapping intervals for the same learner in the same section. Which one wins, and what does the exclusion constraint do to that apply?
- A learner withdrew but has a graded attempt in week 2. What does the gradebook show, and which predicate in your query decides it?
- How do you answer 'which learners did we deprovision last Tuesday' with this schema?
Model external identities so a second platform is not a merge
Easy · identity resolution · normalisation · unique constraints
You have person(person_id, tenant_id, display_name, email_lower NULL). Learners arrive three ways: a signed launch carrying (issuer, audience, subject, deployment_id), a roster feed carrying a sourcedId, and SSO carrying a nameid. The same human can arrive from two platforms. Email is frequently absent, recycled between graduating and incoming learners, and never guaranteed unique. Give the DDL for the identity tables with every uniqueness constraint, the statement a launch runs to resolve a person, and the rule that decides when two external identities may be bound to one person.
Approach
- Split the human from its identifiers: person holds the internal id, external_identity holds one row per issued identifier with a FK to person. The relationship is many-to-one by construction, so a second platform is an INSERT, not a reconciliation, and nothing in the schema tempts you to key on a mutable attribute.
- Scope the subject correctly. A subject claim is unique only within its issuer, and a platform that issues pairwise subject identifiers emits a different subject per relying party, so the natural key is (issuer, audience, subject); dropping audience collides the first time the same platform is registered twice. Tenancy comes from the deployment mapping you store, never from anything the client sends.
- Give roster identifiers their own namespace: UNIQUE (tenant_id, source_system, sourced_id), either as a discriminated row in the same table or its own table. Cross-namespace binding is where duplicate humans are born, so require a corroborating signal — a sourcedId released inside the launch, or an explicit admin action — and never a name or email string match.
- Write the resolve as an atomic claim rather than SELECT-then-INSERT: INSERT INTO external_identity (...) VALUES (...) ON CONFLICT (issuer, audience, subject) DO NOTHING RETURNING person_id, and on zero rows re-SELECT. Allocate the person inside the same transaction so losing the race rolls the speculative person back; two simultaneous first launches must leave exactly one person.
- Make merges explicit and reversible-by-audit: person_merge(loser_person_id PRIMARY KEY, winner_person_id, merged_at, actor, reason) plus a redirect on lookup, because stored attempts, scores and outbound publications still carry the loser's id and must keep resolving. A merge is one-way; an unmerge is a restore from the audit, not an UPDATE.
- Keep email as a searchable attribute, case-folded, with a non-unique index for support lookup only, and state the rule out loud: a support tool may surface candidates, it may not auto-bind them.
Worked solution (20 min)
- Write external_identity with UNIQUE (issuer, audience, subject) and a separate UNIQUE (tenant_id, source_system, sourced_id) for feed identifiers, both FK to person.
- Write the ON CONFLICT DO NOTHING RETURNING claim plus the zero-row fallback SELECT, inside one transaction with the person insert.
- Run two concurrent first launches with the same tuple and assert one person row and two identical resolutions.
- Insert a second identity for the same human from a different issuer, bind it to the existing person, and show that no constraint had to be dropped to allow it.
Expected result: One person per human, one external_identity row per issued identifier, uniqueness enforced on (issuer, audience, subject) and on (tenant_id, source_system, sourced_id), and no unique constraint anywhere on email.
Verify it yourself
- Two identities differing only in issuer both insert successfully and resolve to the same person_id.
- Two concurrent first launches produce exactly one person row, with the loser returning the winner's person_id rather than an error.
- Inserting two people with the same email_lower succeeds, and the resolve path never reads that column.
- After a merge, a lookup by the loser's person_id still resolves, through the redirect, to the winner.
Follow-up
- A district changes platforms. Every learner arrives with a new issuer and subject but the same sourcedId. What does the first launch after the change do, and how many person rows exist afterwards?
- Two identities were bound to one person in error and both have graded attempts. What does the unmerge do, and which stored ids can you not change?
- The launch releases no name and no email at all. What does the instructor see in the roster, and where does that display name come from?
Bases de programacion tanto de programacion en general asi como del lenguaje.
Medium
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?
These questions assess your core understanding of programming principles and your ability to apply them effectively in your daily work.
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Design the error contract for a learner attempt client
Easy · error taxonomy · retries · offline clients
The caller is the in-browser attempt client. It queues writes while offline and drains that queue on reconnect, so every response it gets is fed to one retry policy. Design the error contract for the attempt endpoints: the media type, the fields every error carries, and the classification the client switches on. Cover schema validation, an expired session, a stale autosave sequence, a write after the deadline, the attempt cap, an id belonging to another tenant, overload, and an indeterminate outcome. For each give a status, a stable code, whether retrying can help, and what the learner is told.
Approach
- Fix the envelope first: application/problem+json (RFC 9457, which obsoletes RFC 7807) carrying type, title, status, detail and instance, plus two extension members, a stable machine code and a retry class of never, after_backoff or resync_then_retry. The prose is for humans and must stay free to change; the client switches only on the two extensions.
- Classify on two axes rather than by status code: is the stored state known, and can the client change anything by resending. A stale autosave sequence is known and only fixable by resyncing, so it is 409 with the server's current last_autosave_seq in the body; resending the same payload forever is the exact failure the taxonomy exists to prevent.
- Treat a past-deadline write and an exhausted attempt cap as terminal business outcomes, not transport errors. They are 409 or 422 with codes the client turns into a final, visible message and removes from the queue. Returned as 5xx or as an untyped 4xx, they cycle in the offline queue and the learner watches a permanent 'saving' spinner.
- Return the same 404 for an id from another tenant as for an id that does not exist. A 403 confirms the row exists somewhere, which is an enumeration oracle across tenants.
- Give session expiry inside an embedded frame its own code: a partitioned or blocked third-party cookie means the client cannot silently refresh, so the response must tell it to prompt a re-launch rather than retry into a loop.
- Pin the taxonomy with a test asserting every handler path emits a code from the enumerated set, because the uncoded catch-all 500 is what makes a client's switch fall through to its default branch, and that branch is always retry.
Worked solution (20 min)
- Write the problem+json schema with code and retry_class as named extension members and an enumerated code list.
- Enumerate the eight failures from the prompt plus two more you find in the handlers, and place each in a table with status, code, retry_class and the one-line client action.
- Re-read the deadline, attempt-cap and validation rows and confirm every one is retry_class never and is dropped from the queue.
- Write the client's switch in pseudocode and check that every branch is reachable from the table and that nothing reaches a default that retries.
- Send a request for an attempt id from a second tenant and confirm the response is byte-identical to the response for a nonexistent id.
Expected result: A table of at least ten codes in which exactly three classes can be retried (overload with backoff, an indeterminate 5xx, and a stale sequence after resyncing), deadline, cap and validation rows are terminal and leave the queue, and a cross-tenant id is indistinguishable from a missing one.
Verify it yourself
- A client implementing only the retry_class column never retries a past-deadline write.
- Every error carries a code distinct from its status, so two different 409s are distinguishable without parsing prose.
- No error body echoes a field from another tenant's row or any part of an answer key.
Follow-up
- The client drains forty queued autosaves an hour later, but the attempt was submitted from another device meanwhile. What does each write get back, and what does the learner see?
- How do you add a new code later without breaking a client that switches exhaustively over the current set?
- Which of your codes are safe to render verbatim to a learner, and which leak something about another tenant or about the answer key?
A key rotation at one platform takes down every tenant
Medium · dependency outage · retry amplification · lock convoy · bulkheads
At 08:00 launches begin failing with 'unknown key id' for 3 of 140 tenants. Within 90 seconds, p99 launch latency on every pod rises to 3 s and fleet-wide error rate hits 12%, including tenants on unrelated platforms. The upstream identity platform returns 429. Our outbound request rate to its key endpoint is 300/s, up from 0.02/s. On an unknown key id the verifier refetches the key set synchronously while holding the write lock on the shared key cache. Give an ordered diagnostic checklist, the root cause of the fleet-wide blast radius, and the fix.
Approach
- Order the checklist around the contradiction in the symptom, which is the fastest path in. First, scope the failure: 3 tenants fail with a specific error, but 140 tenants are slow. Two different populations means two different causes, and the interesting one is the second. Second, check outbound request rate to the dependency before checking the dependency's health, because a 15,000x increase in our own traffic reframes the 429 as self-inflicted. Third, take a thread or goroutine dump and look at what the blocked requests are waiting on. Fourth, read the code path that connects the two.
- Establish the trigger: one platform rotated its signing key, so every launch from its 3 tenants presents a key id the cache does not have. At bell volume that is thousands of cache misses within seconds, each one a synchronous fetch, with no single flight, no rate limit, and no negative cache. Our own retry behaviour produced the 429 that then fails those launches, which produces more misses.
- Establish the amplification separately, because it is the real finding: the refetch holds the shared cache write lock across network I/O. Every other launch on that pod, including ones whose key id is already cached and whose platform is fine, blocks behind a multi-second HTTP call. A per-tenant dependency failure becomes a fleet-wide latency failure through a lock, not through capacity.
- Fix the amplification first: never hold a lock across network I/O. Fetch outside the lock and swap the result in, or shard the cache per issuer so one issuer's refresh cannot block another's reads. That alone confines the blast radius to the 3 affected tenants.
- Then bound the fan-out: single flight per issuer so at most one refetch is in flight, a token bucket on refetches per issuer, and a short negative cache for an unknown key id so a forged or stale token cannot become a request flood. Serve the currently cached keys while a refresh is in progress rather than blocking on it.
- Then remove the trigger: refresh key sets proactively on a schedule and on the cache-control lifetime the endpoint advertises, so a rotation is absorbed before a launch depends on it. Reactive fetching on the critical path guarantees that every rotation is first observed as user-visible failure.
Follow-up
- The platform rotates without warning again while its key endpoint is genuinely down for 20 minutes. What does a launch see, and what do you cache to survive it?
- Where else in this system does a shared lock turn a single-tenant dependency failure into a fleet-wide one?
- How would you prove the negative cache cannot be used to lock out a tenant whose key id is legitimately new?
Suggested practice checklist
For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.
Day 1 — Numbers before diagrams
- Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
- Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
- Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.
Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.
Day 2 — One system, from requirements to schema
- Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
- Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
- Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.
Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.
Day 3 — The consistency you are actually buying
- Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
- Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
- Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.
Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.
Day 4 — Failure is the design
- For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
- Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
- Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
- Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.
Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.
Day 5 — Scaling the hot path
- Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
- Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
- Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
- Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.
Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.
Day 6 — Keep the coding hand in, at the bar that applies to you
- Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
- Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
- Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.
Deliverable: One problem hardened to review standard, with its test list and one written review comment.
Day 7 — Defend it while being interrupted
- Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
- Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
- Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.
Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.
Disagreement needs an outcome you did not choose
Conflict answers where you were right and everyone came round are the weakest ones. Stronger: the evidence you went and collected, what would have changed your mind, and what you did in the weeks after the call went against you. Implementing a design you argued against, properly, is a specific and checkable behaviour.
What is your ideal work environment?
Medium · behavioural and engineering judgement
Approach
- 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.
- 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?
Talk a bit about yourself.
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Discuss your experience with version control systems like GitHub.
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
Apptegy values candidates who are thoughtful about their work environment and personal motivations.
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?