cswork / libraryOpen practice workspace

Interview guides · Almabase

Almabase Software Engineer Interview Guide

Almabase Software Engineer interview guide: reported rounds, Django and Python questions, async email design, schema tuning and a 7-day prep plan.

The 60-second brief

Almabase builds a platform that educational institutions use to run their alumni programmes: keeping an alumni network connected, managing community engagement and raising funds. Software Engineers there, as candidates describe the role, work mainly in Python and Django across the whole life of a feature: designing the data model, tuning database schemas, building asynchronous messaging that spans regions, writing the implementation and fixing production issues. The work involves product managers on specifications and other engineers through code review, and the platform is described as data-heavy.

This guide is for Software Engineer candidates interviewing at Almabase, especially engineers who will be asked about Python, Django and SQL. It covers the three reported rounds, the Django-based assignment candidates mention, a worked approach to each reported question across fundamentals, design, production work and behavior, and a seven-day plan that turns those questions into things you build and rehearse.

Candidates report three rounds over roughly 3-5 weeks: a Technical Screening, a Deep-Dive Design discussion, and Behavioral Rounds about fit and past projects. Many also report a project-based assignment, usually in Django.

Focus areas

  • System Design
  • Django (Python Web Framework)
  • Asynchronous Processing

Overview

Almabase builds a platform for educational institutions to manage their alumni: the network itself, engagement with it, and fundraising. Candidates describe the Software Engineer role as work in Python and Django on product features, data models and asynchronous systems: designing and tuning database schemas, building message processing that spans regions, writing low-level designs, shipping features and debugging production problems, alongside product managers and other engineers.

The reported questions track that work closely. Four test fundamentals (an undo/redo feature, method overloading versus overriding, NAT, and a practical coding scenario). Four are design questions (an asynchronous cross-region email system, a database schema optimised for high demand, an alarm clock, and scaling a feature past 100,000 users). Three concern production work (deployment risk, a debugging story, failure states in asynchronous systems) and three are about motivation and teamwork. Asynchronous processing shows up in two of them, so queue semantics, retries and idempotency deserve more of your time than any single algorithm topic.

Candidates also report a project-based assignment, usually in Django, that later technical discussions build on, and behavioral rounds about past project experience. Your own code is therefore a large part of the loop: you should be able to explain every model, query and library choice in the assignment and on your resume, and say what you would change. The requirements candidates describe list Python, Django, SQL and database design as must-haves and Celery or Redis task queues and cloud infrastructure as nice-to-haves, which sets the priority order for the plan below.

The Almabase interview process

Candidates report three rounds over roughly 3-5 weeks: Technical Screening, Deep-Dive Design and Behavioral Rounds. Many also mention a project-based assignment, usually in Django, that later technical discussions return to.

Technical Screening

Candidates report the Technical Screening as the first, general technical conversation; no format or question list is reported for it. Because the stage is described only in general terms, prepare broadly. Separately from any round, the reported question set includes a fundamentals category (undo/redo, overloading versus overriding, NAT, a practical coding scenario) that is worth covering in general, and a short, accurate account of your Python, Django and SQL work covers the fit side. Answer conceptual questions with a definition, a concrete example and one edge case, and talk through your reasoning while you write code.

What to demonstrate

  • Technical skills, as candidates report the stage
  • Fit for the role, including the Python and Django background the requirements list

How to prepare

  • Explain overloading versus overriding in Java and in Python, with a functools.singledispatch example and a Django model save() override that calls super().
  • Implement undo/redo with the Command pattern, two stacks and a capped history.
  • Walk through one NAT translation-table entry for an outbound connection and its reply, and say why an unsolicited inbound packet is dropped.
  • Prepare a short background summary that names only the Python, Django and SQL work you can discuss in depth.

Deep-Dive Design

Candidates report Deep-Dive Design as a design discussion; no further format is reported. Candidates do not tie specific questions to this round, but the reported design-category questions are the ones to prepare for: they range from object-level design (an alarm clock built from the ground up) to system design (an asynchronous cross-region email pipeline, a schema optimised for high demand, scaling a feature to 100,000 users). Approach each the same way: clarify scope and numbers, outline the components and data model, then spend most of the discussion on failure handling, scaling and the trade-offs you chose. Candidates report that the Django assignment feeds later technical discussions, so be ready to extend or critique your own design too.

What to demonstrate

  • Design principles: clear object responsibilities, extensibility, testability
  • System-level thinking: components, data flow, scaling and failure handling
  • Reasoning about trade-offs and adjusting the design as the discussion moves

How to prepare

  • Whiteboard the asynchronous email pipeline end to end: outbox relay (or on_commit plus a sweeper), regional queues, retry policy, dead-letter queue, idempotent send.
  • Design the alarm clock with a state machine, a min-heap scheduler, an interruptible wait and an injectable clock, and list the daylight-saving edge cases.
  • Model an alumni-style schema (institutions, members, events, donations), write its indexes, and read the SQL the Django ORM generates for the main pages.
  • Do the 100,000-user capacity estimate on paper and list the fixes in order of cost.

Behavioral Rounds

Candidates say the Behavioral Rounds center on how you would fit the team and on projects you have already built. The reported behavioral-category questions, which candidates do not attribute to a specific round, cover motivation for joining a startup, handling technical design disagreements, and contributions to past projects, hackathons or internships. Treat project questions as technical conversations: know the architecture, your exact part, the numbers and the reason behind each choice. For motivation and disagreement prompts, use one specific story each, told in the first person, with a clear outcome.

What to demonstrate

  • Depth and ownership in past projects
  • Collaboration, including how design disagreements get resolved
  • Motivation for a startup role

How to prepare

  • Write a decision log for two or three resume projects and the project-based assignment: the choice, the alternatives, the cost, what you would change.
  • Prepare one design-disagreement story with criteria, evidence and outcome, ideally one where you changed your mind.
  • Write a motivation answer that links your past ownership to an alumni and fundraising product built on Django.
  • Rehearse a production incident story with impact, mitigation, root cause and prevention.

Where candidates lose points

Answering the asynchronous email design with 'put it on a queue and retry' and stopping there.

State the delivery guarantee (at-least-once), how a committed email is guaranteed to be enqueued (an outbox relay, or on_commit plus a sweeper), how a send stays idempotent, which errors retry and which are permanent, where exhausted messages go (dead-letter queue plus replay), and how bulk mail is kept from delaying transactional mail. The failure-states question in the reported set covers the same ground, so one well-practised answer serves both.

Describing a schema without knowing what SQL the Django ORM actually sends.

Before the loop, build a small Django app, turn on query logging (django-debug-toolbar or connection.queries), and fix an N+1 with select_related and prefetch_related. Run EXPLAIN ANALYZE on the resulting SQL so you can discuss joins, foreign keys and indexes from evidence; SQL and database design are among the must-have skills candidates list.

Claiming Python supports method overloading the way Java does.

Know that a second def with the same name replaces the first, and have the real alternatives ready: default and keyword arguments, *args, and functools.singledispatch. Pair it with a Django override example (save() or get_queryset() calling super()) so the answer is grounded in the stack candidates report.

Being unable to defend choices in your own project-based assignment or resume projects.

Candidates report a project-based assignment, usually in Django, that later technical discussions build on, and behavioral rounds about past projects. Keep a short decision log for the assignment and each resume project (choice, alternatives, cost, what you would change) and reread it before every round.

Jumping to microservices or sharding for the 100,000-user scaling question.

Start with a back-of-envelope estimate of requests per second and peak load, show that the number is modest, then work through fixes in order of cost: queries and indexes, background workers, caching, pooling, replicas. Name what would break first and how you would measure it before reaching for bigger architecture.

Questions & practice

How do you implement an undo/redo feature in a web application?

Medium · Technical Fundamentals & Coding

Approach

  1. Model every user action as a command object with execute() and undo() (the Command pattern) and keep two stacks: a new action pushes onto the undo stack and clears the redo stack; undo pops, calls undo() and pushes onto redo; redo does the reverse. Each operation is O(1).
  2. Name the alternative: snapshot (Memento) history stores whole states, which is simple and safe for complex edits but costs memory per step; inverse commands are compact but every command needs a correct inverse. Editors often mix them, e.g. periodic snapshots plus deltas.
  3. Bound the history (a deque with maxlen drops the oldest entry) and coalesce fine-grained events so typing a word becomes one undo step, not one per keystroke.
  4. Decide where history lives in a web app: client-side for UI state such as a form builder, or server-side as a versioned operation log when undo must survive a reload or another device. A server-side undo applies a compensating operation rather than deleting rows from the log.
  5. Cover edge cases: actions with external side effects (a sent email cannot be recalled, so delay the send or offer a cancel window), undo after the record changed elsewhere, and redo after the history branched.

Follow-up

  • How does undo work when two users edit the same document at the same time?
  • How would you persist undo history across page reloads?
  • Which actions would you refuse to make undoable, and how would the UI show that?

Can you explain the difference between method overloading and overriding?

Medium · Technical Fundamentals & Coding

Approach

  1. Overloading: several methods with the same name but different parameter lists in one class, chosen at compile time from the argument types (static dispatch) in Java, C++ or C#. A different return type alone does not create an overload.
  2. Overriding: a subclass redefines a method with the same signature as its parent, and the call is resolved at runtime from the object's actual type (dynamic dispatch, via a vtable in C++ and Java). This is runtime polymorphism: a Shape reference calling area() runs Circle.area().
  3. Say how Python differs, since Python is the stack candidates report: a second def with the same name simply replaces the first, so there is no true overloading. Emulate it with default or keyword arguments, *args, or functools.singledispatch / singledispatchmethod, which dispatch on the first argument's type; typing.overload only informs type checkers.
  4. Give a Django example of overriding: a model's save() or a class-based view's get_queryset() or form_valid(), calling super() so the parent's behaviour still runs. Forgetting super().save() is a classic bug.
  5. Mention Java edge cases: static methods are hidden, not overridden; private and final methods cannot be overridden; an override may use a covariant return type; @Override turns a signature typo into a compile error.

Follow-up

  • Under multiple inheritance in Python, how does super() decide which parent method runs (MRO, C3 linearization)?
  • What is the difference between overriding and method hiding?
  • When would you prefer singledispatch over an isinstance chain?

What is Network Address Translation (NAT) and how does it function?

Medium · Technical Fundamentals & Coding

Approach

  1. Define it: NAT rewrites IP addresses, and usually ports, in packet headers as they cross a router, so hosts with private addresses (the RFC 1918 ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) can reach the internet through one or a few public addresses.
  2. Walk the mechanics of the common form, PAT/NAPT: an outbound packet from 192.168.1.10:51000 gets its source rewritten to the public IP and a free port, say 203.0.113.5:62001; the router stores that mapping in its translation table, and replies addressed to 203.0.113.5:62001 are rewritten back and forwarded inside.
  3. Name the variants: static NAT (fixed one-to-one mapping, used to expose a server), dynamic NAT (one-to-one from an address pool) and PAT (many-to-one using ports, the home-router default).
  4. Give the consequences: it stretched the IPv4 address space; unsolicited inbound traffic has no table entry and is dropped unless port forwarding is set up; it breaks end-to-end addressing, so peer-to-peer apps need STUN/TURN or hole punching; idle mappings expire, which is why long-lived connections send keepalives. In the cloud, a NAT gateway gives private subnets outbound-only access.

Follow-up

  • How would clients reach a web server that sits behind NAT?
  • Is NAT a firewall? What does it protect against and what does it not?
  • What changes with IPv6?

Write code to solve a practical scenario rather than a standard algorithm puzzle.

Medium · Technical Fundamentals & Coding

Approach

  1. Treat it as a small feature, not a puzzle: restate the requirements, write two or three concrete input/output examples, and agree on what is out of scope before coding. Good practice scenarios include a rate limiter, a CSV import with validation and de-duplication, a booking system with conflicting slots, or an in-memory key-value store with expiry.
  2. Outline the structure before typing: a data model, a service layer holding the business rules, and a thin I/O layer. Keeping rules out of I/O code makes them testable and lets a CLI become a Django view later without rewriting them.
  3. Get one end-to-end path working first, then handle edge cases explicitly: empty or malformed input, duplicates, boundary values, time zones, concurrent updates. Raise specific exceptions instead of silently returning None.
  4. Write a few tests (pytest or Django's TestCase) for the main path and each edge case you named, and state the complexity of the core operation, e.g. O(1) lookups through a dict keyed by the natural id.
  5. Close with extensibility: show where a new requirement (another validation rule, a different storage backend) plugs in without editing existing classes.

Follow-up

  • A new requirement arrives: how much of your code has to change?
  • How does this behave with a million records instead of a hundred?
  • How would you test the concurrent case?

Describe the database schema for a complex application; how would you optimize it for high demand?

Medium · System Design & Architecture

Approach

  1. Pick a concrete domain and say it out loud, e.g. institutions, members, events, registrations and donations. Start from the access patterns (members of one institution filtered by graduation year, donation totals per campaign) and design tables in third normal form, with foreign keys and unique constraints enforcing the real-world rules.
  2. Index for queries, not columns. In a composite index, lead with the equality column nearly every query filters on (here the tenant, institution_id), put the other equality columns next and range columns last, e.g. (institution_id, status, created_at); do not order by selectivity alone. Add partial indexes for hot subsets and indexes on foreign keys you join or filter on. Each index slows writes, so justify it with a query.
  3. Show you know what the ORM emits: select_related for foreign-key and one-to-one joins, prefetch_related for reverse and many-to-many relations (a second query with IN), only()/values() to skip wide columns, bulk_create/bulk_update for batches, and EXPLAIN ANALYZE to confirm the plan. Name N+1 queries as the usual cause of slow Django pages.
  4. Under high demand, work in order: fix queries and indexes, use keyset pagination instead of large OFFSETs, add connection pooling, cache hot reads in Redis with explicit invalidation, send reporting to read replicas, keep summary tables or materialized views for totals, and partition append-only tables such as email events. Denormalize only where a measured read path needs it.
  5. Schema changes under load: add columns as nullable and backfill in batches. A plain SET NOT NULL scans the table under an ACCESS EXCLUSIVE lock, so in Postgres add CHECK (col IS NOT NULL) NOT VALID, then VALIDATE CONSTRAINT (a weaker lock), then SET NOT NULL, which PG12+ proves from the check without a scan. Add foreign keys as NOT VALID and validate them afterwards.

Follow-up

  • How would you denormalise a parent's key onto a large child table and backfill it while the app is live?
  • How do you catch N+1 queries before they reach production?
  • When would you shard, and what would you shard on?

Denormalise tenant onto revisions and backfill it live

Hard · denormalisation · online migration · backfill

resource_revision (revision_id, resource_id, version, actor_user_id, change_kind, patch, request_id, created_at) has 400M rows and no tenant column; tenant_id lives only on resource. Two reads need it: a tenant-scoped audit feed ordered by created_at DESC, and an offboarding purge. Both join back to resource today. Justify adding tenant_id to resource_revision against those two reads, name the anomaly the copy introduces and the constraint that prevents it, then give the ordered migration for a live table taking 1.2k writes/second — the lock each step takes, how the backfill is batched, and where each step stops being reversible. PostgreSQL 16.

Approach

  1. Justify from the access path rather than from taste. Without the column, the audit feed either scans resource_revision by created_at and discards other tenants' rows, or resolves the tenant's resource_ids first and probes with them — both proportional to the tenant's whole history rather than to one page. With (tenant_id, created_at DESC, revision_id DESC) it is a seek that stops at 50 rows, and the purge becomes a ranged delete instead of a join.
  2. Name the cost exactly: a second copy of a fact can disagree with the first. Make the disagreement unwritable rather than documented — add UNIQUE (resource_id, tenant_id) on resource so it can serve as a foreign-key target, then FOREIGN KEY (resource_id, tenant_id) REFERENCES resource (resource_id, tenant_id) on the revision table. A revision can then only ever carry its parent's tenant.
  3. Step one, expand: ALTER TABLE resource_revision ADD COLUMN tenant_id BIGINT NULL, with no default, so it is a catalogue change and no rewrite. It still needs ACCESS EXCLUSIVE for an instant, and that instant queues behind the longest open transaction on the table while every later query queues behind it — set lock_timeout to 2s and retry rather than wait.
  4. Step two, dual-write: deploy the writer that populates tenant_id on every new revision while reads still use the join. Reversible by redeploying the previous build, because nothing reads the column yet.

Follow-up

  • The backfill is half finished and a rollback is required. What state is the table in, and what does the previous build do with a half-populated column?
  • How do you verify the backfill actually finished, given rows are still being inserted while it runs?

How would you design an asynchronous email processing system that handles cross-region queues and retries?

Medium · System Design & Architecture

Approach

  1. Scope first: transactional mail (password resets, receipts) versus bulk campaigns, expected volume, which regions, and whether data must stay in its region. Those answers drive queue separation and routing.
  2. Write path: in the request's transaction, insert an email row (recipient, template, payload, status=pending, idempotency key under a unique constraint). Only a transactional outbox read by a relay guarantees a committed email is never lost and a rolled-back one never sent. Django's transaction.on_commit only stops the task running before the commit; if the process crashes or the broker publish fails after commit, nothing is enqueued, so pair it with a sweeper that re-enqueues pending rows older than N minutes. Region-local queues (Celery on Redis, SQS or Pub/Sub) feed the workers.
  3. Retries: classify errors. Timeouts, 429s and 5xx responses retry with exponential backoff and jitter up to a cap; hard bounces and invalid addresses fail permanently and go on a suppression list. After the last attempt the message moves to a dead-letter queue with an alert and a replay tool.
  4. Delivery is at-least-once, so make sending idempotent: claim the row atomically (UPDATE ... SET status='sending' WHERE id=... AND status='pending'), pass the provider an idempotency key if it supports one, and accept that a crash between send and status update can duplicate a mail. On a Redis broker, keep the visibility timeout above the longest task or it gets redelivered.
  5. Cross-region and scale: route by tenant or recipient region, fail over to another region's provider when one is down, keep transactional and bulk traffic on separate queues so a campaign cannot starve password resets, rate-limit per provider, and monitor queue depth, oldest-message age and bounce rate.

Follow-up

  • A worker crashes after the provider accepted the mail but before the status update. What happens?
  • The web process dies right after the commit but before the task is published. How does the email still go out?
  • One region's queue is down for an hour: what do users see, and what happens to the backlog?

Design an alarm clock system from the ground up.

Medium · System Design & Architecture

Approach

  1. Clarify which alarm clock: a single device or app (object-oriented, low-level design) or a service firing alarms for many users. Pick one, say why, and list the features: one-off and repeating alarms, snooze, dismiss, labels, enable/disable.
  2. Classes: Alarm (id, time of day, repeat days, snooze minutes, enabled), AlarmScheduler, a Clock interface you can swap for a fake clock in tests, and a Notifier strategy (sound, vibration, push). Model the lifecycle as a state machine: scheduled, ringing, snoozed, dismissed.
  3. Scheduling: keep upcoming triggers in a min-heap keyed by next fire time and sleep until the top entry instead of polling every second; push and pop are O(log n). When an alarm is added or edited to fire earlier than the current top, wake the scheduler (a condition variable notify or an interruptible wait) so it recomputes its sleep, or the new alarm fires late. After a repeating alarm fires, push its next occurrence. Persist alarms so a reboot rebuilds the heap.
  4. Edge cases: daylight-saving changes (a 2:30 alarm on a spring-forward day does not exist; on fall-back the time occurs twice), the user changing time zone, two alarms in the same minute, a snooze that overlaps the next alarm, editing an alarm while it rings. Store alarms as local wall time and recompute the absolute instant.
  5. Multi-user version: store next_fire_at in UTC with an index, let workers claim due rows with SELECT ... FOR UPDATE SKIP LOCKED or use a delayed queue, and make firing idempotent because delivery is at-least-once.

Follow-up

  • The scheduler is sleeping until tomorrow's alarm and the user adds one for a minute from now. How does it fire on time?
  • How do you test the scheduler without waiting for real time to pass?
  • What changes at ten million users?

How do you approach scaling a feature to support over 100,000 users?

Medium · System Design & Architecture

Approach

  1. Pin down what 100,000 users means: registered, daily active or concurrent. Do the arithmetic aloud: 100,000 daily users making 20 requests each is 2 million requests a day, about 23 per second on average; a 10x peak is roughly 230 per second, which a few stateless app servers and one well-indexed Postgres instance can usually serve.
  2. So hunt hot spots rather than redesigning: queries per request (N+1), unindexed filters, heavy reports, and fan-out actions such as notifying every member of a group at once. Measure with APM and the slow-query log before changing anything.
  3. Apply fixes in order of cost: query and index fixes; moving slow work (email, exports, imports) to background workers such as Celery; caching hot reads in Redis with a clear invalidation rule; connection pooling; read replicas; a CDN for static files and media. Keep app servers stateless so they scale horizontally behind a load balancer.
  4. Ship it safely: load-test the expected peak with a tool such as Locust, release behind a feature flag to a percentage of users, and watch latency percentiles, error rate and queue depth.
  5. Name what fails under spikes: cache stampedes when a hot key expires (use a lock or staggered TTLs) and bursts of background jobs that exhaust database connections.

Follow-up

  • What breaks first in your design, and how would you know?
  • Which parts change at a million users?
  • How do you keep cached data consistent when the underlying record changes?

What is your approach to handling failure states in an asynchronous architecture?

Medium · Production Debugging & Problem Solving

Approach

  1. Classify failures first: transient (timeouts, rate limits, a dependency briefly down), permanent (bad input, a deleted record) and poison messages that crash the consumer every time. Each class gets its own handling path.
  2. Transient: retry with exponential backoff and jitter, a maximum attempt count, and a timeout on every external call; add a circuit breaker so a dead dependency is not hammered. Permanent or exhausted: record the failure and move the message to a dead-letter queue with alerting and a safe replay tool.
  3. Assume at-least-once delivery: consumers must be idempotent (idempotency keys, unique constraints, conditional status updates). Acknowledge only after success (Celery acks_late) so an exception or broker disconnect leads to redelivery. If the worker child process is killed, Celery still acks by default; set task_reject_on_worker_lost=True, with idempotent tasks, to get redelivery in that case too.
  4. Solve the dual-write problem. transaction.on_commit fixes the ordering half (the task no longer runs before the row exists or after a rollback) but not the loss half (a crash between commit and publish drops the task). Only a transactional outbox read by a relay, or on_commit plus a sweeper that re-enqueues stuck pending rows, closes both.
  5. Make state visible: each job row carries a status (pending, running, succeeded, failed, dead), timestamps and an attempt count so a sweeper finds stuck jobs; dashboards track queue depth, oldest-message age and retry rate. For multi-step workflows, use compensating actions (sagas) instead of distributed transactions.

Follow-up

  • Is exactly-once processing possible here? What would you settle for?
  • How do you replay a dead-letter queue without repeating side effects?
  • A downstream service is down for an hour: what happens to the queue and to users?

Can you walk through a time you debugged a complex issue in a production environment?

Medium · Production Debugging & Problem Solving

Approach

  1. Structure it as: symptom and impact (who was affected, how badly, how it was detected), mitigation first (rollback, disabling a flag, scaling up), investigation, root cause, fix, prevention. Keep the setup short and spend the time on reasoning.
  2. Show the debugging sequence: list what changed (deploys, config, traffic, data shape, dependency versions), form hypotheses, and test each with logs, metrics, traces or a reproduction on production-like data, saying what each check ruled out.
  3. Separate trigger from cause: a deploy can expose a race condition, a missing index or a retry storm that was always there. Strong examples include an N+1 query that only hurt one large customer, a Celery task redelivered because it outlived the broker's visibility timeout, or a time-zone bug at a DST change.
  4. End with numbers and prevention: latency or error rate before and after, and the test, alert or runbook you added so it cannot recur silently. Say "I" for your part and credit others for theirs.

Follow-up

  • With hindsight, what would you have checked first?
  • How did you confirm the fix worked rather than the problem just going quiet?
  • What if you had not been able to reproduce it?

Suggested practice checklist

Each day takes one cluster of reported questions plus the stack candidates list (Python, Django, SQL, Celery and Redis) and ends with something you build or write that you can reuse in the rounds.

Day 1 — Python and OOP fundamentals

  • Implement undo/redo in Python with a Command interface, two stacks and a deque-capped history; add a test that a new action clears the redo stack.
  • Write a one-page comparison of overloading and overriding with Java examples and the Python equivalents (default arguments, functools.singledispatch, super() in a Django model save()).
  • Sketch a NAT translation table for two internal hosts connecting to the same server, and explain what happens to an unsolicited inbound packet.

Deliverable: A tested undo/redo module, an overloading-versus-overriding comparison page, and an annotated NAT table sketch.

Day 2 — Django ORM, SQL and schema design

  • Model institutions, members, events and donations in Django; log the SQL for a member list page and remove an N+1 with select_related and prefetch_related.
  • Add a composite index led by institution_id and a partial index for the two main filters, and compare EXPLAIN ANALYZE output before and after.
  • Work the practice question on denormalising a tenant key onto revisions and backfilling it live: batch size, locking, and adding NOT NULL through a NOT VALID check constraint and VALIDATE.

Deliverable: A small Django project with before-and-after query counts, two EXPLAIN plans, and a written live-backfill plan.

Day 3 — Practical coding and the project-based assignment

  • Pick a feature-shaped problem (a CSV member import with validation and de-duplication, or an expiring key-value store) and build it with a model, a service layer and tests, under a time limit you set yourself.
  • Write a README section listing each design choice and its alternative, as you would for the Django assignment.
  • Review the result against an edge-case list: empty input, duplicates, malformed rows, time zones.

Deliverable: A working, tested feature with a README decision list and a completed edge-case checklist.

Day 4 — Asynchronous processing and failure states

  • Build a Celery task on a local Redis broker that sends email through a fake provider that fails at random; configure retries with backoff and jitter, acks_late, task_reject_on_worker_lost and a maximum retry count.
  • Make the send idempotent with a status row and a conditional update, write an outbox row in the same transaction with a relay that enqueues it (or use on_commit plus a sweeper for stuck pending rows), and route exhausted messages to a failed table you can replay.
  • Whiteboard the cross-region version: regional queues, routing rule, failover, separate transactional and bulk queues, monitoring.

Deliverable: A runnable, retrying, idempotent email worker and one cross-region pipeline diagram with every failure path labelled.

Day 5 — Object design and scaling

  • Design the alarm clock (class diagram, state machine, min-heap scheduler with an interruptible wait) and implement the scheduler against a fake clock, with tests for a repeating alarm, a newly added earlier alarm and a DST transition.
  • Write the 100,000-user capacity estimate (requests per second, peak, database load) and an ordered list of fixes from query tuning to read replicas.
  • Answer both questions aloud and note where your explanation stalled.

Deliverable: Alarm scheduler code with tests, a one-page capacity estimate, and notes on the weak spots in your spoken answers.

Day 6 — Production risk and debugging stories

  • Write a rollout checklist for a risky change: feature flag, canary, dashboards, rollback trigger, and an expand-contract plan for renaming a column in Django.
  • Write your production debugging story in STAR form: impact, mitigation, hypotheses ruled out, root cause, fix, prevention, with numbers.
  • Tell both aloud and cut anything that does not show your own decisions.

Deliverable: A deploy checklist with a migration plan, and a written, rehearsed incident story.

Day 7 — Projects, motivation and a full run-through

  • Write the decision log for two or three resume projects and your project-based assignment.
  • Write answers to the startup-motivation, design-disagreement and past-contributions prompts, each built on one concrete story.
  • Run a mock loop with a friend: one fundamentals question, one design question from this guide, and the three behavioral prompts.

Deliverable: A project decision log, three written behavioral answers, and notes from the mock loop.

Behavioural preparation

Candidates describe the behavioral rounds as focused on fit and past project experience, and the reported behavioral prompts cover startup motivation, design disagreements and contributions to past projects. Prepare stories that contain your own decisions, and make sure each project story holds up to technical questions about why you built it that way.

  1. Can you walk through a time you debugged a complex issue in a production environment?
  2. Which design decision in your project-based assignment or a recent project would you change now, and why?

How do you handle production risk mitigation when deploying new code?

Medium · Production Debugging & Problem Solving

Approach

  1. Answer with practices, then one real deploy that used them. Practices: small reviewed changes, CI with tests, a staging environment with realistic data, and feature flags so deploying code and releasing behaviour are separate steps.
  2. Roll out gradually (canary, percentage rollout or blue-green), with dashboards and alerts on error rate and latency in place before release, and a written rollback trigger: the number that makes you revert.
  3. Treat migrations as the riskiest part: use expand-contract (add a nullable column, deploy code writing both, backfill in batches, switch reads, drop the old column in a later release) so any single deploy can be rolled back. In Django, keep schema and data migrations separate and batch RunPython backfills; on Postgres before version 11, adding a column with a default rewrites the whole table.
  4. For the story, use STAR: the change and its risk, the safeguards you chose, what monitoring showed, and the outcome. A story where a flag or canary actually caught a problem beats one where nothing happened.

Follow-up

  • When do you roll back and when do you fix forward?
  • How would you roll back a release whose migration already dropped a column?
  • What did you change in your process after a deploy went wrong?

What is your motivation for joining a startup environment?

Medium · Behavioral & Motivation

Approach

  1. Build the answer in three parts: evidence that you already work well the way startups run (a project you owned end to end, work you covered outside your title), what you want next, and why this company specifically.
  2. Make the company part concrete: say what you understand about a platform educational institutions use for alumni networks, engagement and fundraising, and connect it to work you would enjoy, such as the Django codebase, data modelling or asynchronous messaging.
  3. Show you understand the trade-offs you are choosing: shifting priorities, small teams, less process, owning what you ship in production. Describing how you handled ambiguity before is more convincing than saying you like it.
  4. Avoid what sinks this answer: a generic 'fast growth and learning' line that fits any startup, criticism of a previous employer, or motivation that is only about title or pay.

Follow-up

  • What would you do if priorities changed halfway through your project?
  • What kind of work would you not want to do here?
  • Where do you want to be in two or three years?

How do you handle disagreements on technical design within a team?

Medium · Behavioral & Motivation

Approach

  1. Pick a real technical disagreement with stakes, e.g. a background queue versus a cron job, a normalised schema versus a denormalised read table, or a library choice, and state both positions fairly in one sentence each.
  2. Show the method: you moved the discussion to criteria (latency, cost, operability, time to ship), wrote down the options and trade-offs, and settled the open point with evidence such as a prototype, a benchmark or production data rather than seniority or persistence.
  3. Show how it was decided and what followed: who owned the call, whether you committed to an outcome you disagreed with, and how it turned out. A story where you changed your mind is often stronger than one where you were simply right.
  4. Avoid the failure modes: no real resolution, a colleague cast as unreasonable, or a disagreement about style rather than a design trade-off.

Follow-up

  • What if the decision went against you and later proved wrong?
  • How do you disagree with someone more senior than you?
  • How do you handle the same disagreement in a code review?

Describe your contributions to past projects, hackathons, or internships.

Medium · Behavioral & Motivation

Approach

  1. Pick two or three projects (work, internship, hackathon or the project-based assignment) and for each prepare: the problem, the team size, exactly which parts were yours, the result in numbers, and one thing you would change.
  2. Prepare the reasoning behind each decision, since candidates report deep dives into resume projects: why that database and schema, why Django or another framework, why a queue, which alternatives you rejected and what each choice cost.
  3. Know the internals you claim: if a project lists Celery, be ready to explain retries and acknowledgement; if it lists Postgres, the indexes you added and why. Remove from your resume any technology you cannot discuss in depth.
  4. For hackathons, the interesting part is scoping under pressure: what you cut, what you stubbed, and what you would build properly afterwards. Use "I" for your work and name the team's contribution separately.

Follow-up

  • Why did you choose that database or framework, and what would you pick now?
  • What broke after launch, and how did you find out?
  • With another month on it, what would you build?