Interview guides · BrandStory (India)
BrandStory (India) Software Engineer Interview Guide
BrandStory (India) Software Engineer interview guide: five reported rounds, PHP, array and web-fundamentals questions, and a seven-day prep plan.
The 60-second brief
BrandStory (India) is described as an agency that delivers marketing and branding services to a range of clients. Software Engineers there build, maintain and tune the web applications behind that client work, and candidates describe the role as either a PHP developer or a full-stack developer. The reported day-to-day work covers new features, bug fixes and hooking third-party services into applications that already exist, done alongside project managers and other developers.
This guide is for candidates interviewing for the Software Engineer role, on either the PHP or the full-stack track. It covers the five reported stages, the four fundamentals questions candidates report (arrays, HTTP methods, database connections in PHP, the request lifecycle), the debugging and security scenarios they mention, and how to talk through your own project history. It does not cover pay.
Candidates report five stages over roughly 4-6 weeks: Initial Contact, Skills Discussion, Practical Assessment, Technical Screens and Final Decision. The technical screens are reported to focus on algorithms and project history.
Focus areas
- PHP Development
- Full-Stack Development
- Array Data Structures
Overview
The reported questions are fundamentals of everyday web work, not puzzles: finding the unique elements in an array, the difference between GET and POST, how a PHP application handles database connections, and what happens to a web request between the browser and the server. Candidates also mention two scenario prompts, debugging a slow-loading page and keeping code safe against common web vulnerabilities. Short questions leave little room, so make each answer precise and complete: structured enough to say out loud, with the edge case or trade-off included.
The role itself is client-facing web development in PHP or a full-stack stack, with React or Vue and modern database systems listed as nice-to-haves, and previous agency or client-facing experience also listed. That shapes preparation in three ways. Write PHP you could defend in review: PDO with prepared statements, escaped output, clear function boundaries. Be able to move between the browser, the server and the database when you explain a problem. And have project stories ready, because the technical screens are reported to cover your project history as well as algorithms.
Candidates describe the atmosphere as friendly and the conversation as a dialogue, and they suggest asking clarifying questions and talking through your reasoning if you get stuck. Use that: state your assumptions on the array question (distinct values, or values that occur exactly once?), ask which database and framework a scenario assumes, and explain why you chose an approach before you defend it. The same habits help when you explain a technical decision to a project manager or client.
The BrandStory (India) interview process
These are the stages candidates describe, in the order they describe them. The reported questions are not tied to specific rounds, so prepare all of them for the technical stages.
Initial Contact
Candidates report that this first stage is a conversation about your application, where expectations for the rest of the process are set. Nothing technical is reported here. Treat it as the place to make your profile easy to place: whether your strength is PHP or full-stack, what you have shipped, and what you want to know about the process and the timeline.
What to demonstrate
- How clearly you describe your background and the track you fit (PHP or full-stack)
- Whether your experience lines up with the requirements candidates describe for the role
- Your questions about the process and next steps
How to prepare
- Write a short spoken summary of your PHP or full-stack experience that names the languages, frameworks and databases you have actually used
- List two or three projects you could expand on later, since project history comes up again in the technical screens
- Prepare questions about how the remaining stages are organised and what the Practical Assessment looks like
Skills Discussion
Candidates describe a broad conversation about your background, centred on the skills the role calls for. Come with concrete examples of what you built and which tool you used for each part, so the conversation has specifics to work with, and be ready to say why you picked each tool over an alternative.
What to demonstrate
- How well your listed skills hold up when you explain them in your own words
- Fit between your experience and the PHP, data structures and framework requirements candidates describe
- Clarity when you explain why you chose a tool or an approach
How to prepare
- Build a skills inventory with one concrete example per line: PHP, a framework, SQL queries, a front-end library if you have one
- Practise explaining how your last project connected to its database and where user input entered it
- For each tool in your inventory, write one sentence on why you chose it over an alternative, since candidates suggest showing why you use specific tools
Practical Assessment
Candidates describe this stage as a hands-on check of your coding, using practical tasks or coding challenges. The format is not described in more detail, so prepare for both a small applied task in PHP or your full-stack language and a short algorithmic exercise. As the coding stage, it is where your coding practice pays off: working code, sensible structure and edge cases handled.
What to demonstrate
- Working code on a small, concrete task
- Correct handling of edge cases in your solution
- Readable, maintainable structure and sensible naming
How to prepare
- Write small array and string functions in PHP (de-duplication, counting, grouping) in more than one way and state the complexity of each
- Build a small page or endpoint that reads from a database through PDO with prepared statements and escapes what it prints
- Run your code against empty input, a single element, duplicates, mixed types such as 1 and "1", and a large input
Technical Screens
Candidates describe the technical screens as covering algorithms and the projects you have worked on. Expect two kinds of conversation: algorithm problems where you reason aloud about complexity, and a walk through projects you built, including the technical challenges you met and how you got past them. Prepare both halves with equal care.
What to demonstrate
- Algorithmic reasoning, including time and space trade-offs
- How accurately you describe what you personally built in past projects
- Clear explanation of the technical challenges in your past projects
How to prepare
- As general review, work array and string problems with a hash map or set, saying the complexity before and after each improvement
- Pick two projects and write the architecture, your own contribution, the hardest bug and what you would change
- For each project, pick one performance or security problem you solved and be able to explain how you measured or verified the fix
Final Decision
Candidates say this last stage is when you hear the result. No technical content is reported for it, so the preparation is about the process: keep your notes from earlier stages, know what you would ask about next steps, and be ready to discuss the offer or the feedback you receive.
What to demonstrate
- Nothing new is reported to be tested at this stage
- The outcome of the earlier stages is communicated to you
How to prepare
- Keep a list of what you were told in each earlier stage so you can ask follow-up questions on the specifics
- Prepare questions about start date, team and project assignment, and how the role splits between PHP and full-stack work
- Keep any other processes you are in moving in parallel, since the full loop is reported to run about 4-6 weeks
Where candidates lose points
Answering the unique-elements question with nested loops and no stated complexity, or reaching for array_unique without knowing what it does
Ask whether "unique" means distinct values or values that occur exactly once. Give the nested-loop answer as the baseline at O(n^2), then a hash set in O(n) time and O(n) space. If you use array_unique, know that it compares values as strings by default and keeps the original keys (wrap it in array_values to reindex). Since PHP 7.2 the default flag uses a hash table, but SORT_REGULAR sorts internally and costs O(n log n).
Explaining GET versus POST as "GET shows data in the URL, POST hides it" and calling POST secure
Cover semantics (GET reads and should be safe and idempotent, POST changes state), caching and bookmarking, URL length limits, and where data ends up in logs and browser history. Add that POST is not encrypted or safe on its own: you still need HTTPS, CSRF protection and server-side validation.
Describing database access in PHP with concatenated SQL strings, legacy mysql_* calls or credentials hard-coded in a file in the repository
Describe PDO with exceptions enabled, bound parameters, an allowlist for dynamic identifiers, and credentials loaded from environment configuration. Practise saying it once per request lifecycle so the connection story and the injection story are one answer.
Describing the request lifecycle as "the browser asks the server and the server sends back the page"
Walk the stages in order: URL parsing, DNS lookup, TCP and TLS handshakes, the HTTP request, the web server handing the request to PHP, the application querying the database, the response with status and headers, and the browser rendering and loading further assets. Then name where you would look first if one stage were slow.
Giving vague project answers where it is unclear what you built, what went wrong and what you would change
For each project, write the problem, your own part, one technical challenge with the fix, and one thing you would redo. Be ready to open with the architecture in a few sentences and let the conversation go deeper where it wants to.
Questions & practice
Find the unique elements in an array.
Medium · Technical Fundamentals
Approach
- Settle the meaning before coding: 'unique' can mean the distinct values (drop duplicates, so [3,1,3,2] gives [3,1,2]) or the values that occur exactly once (that gives [1,2]). Ask which one, and ask whether the output must keep first-occurrence order.
- Distinct values: one pass with a hash set, appending a value to the output the first time it is seen. This is O(n) expected time and O(n) extra space, and it keeps the original order. The brute-force version checks each element against the output list, which is O(n^2).
- Exactly-once values: first build a frequency map (value to count), then make a second pass over the input and keep the values whose count is 1. That is still O(n) time and O(n) space, and walking the input rather than the map keeps the input order.
- If extra memory matters more than order, sort first, which is O(n log n). Equal values are then adjacent, so a single scan comparing each element with its neighbour finds both the duplicates and the singletons. If the input is already sorted, a two-pointer in-place compaction runs in O(n) time with O(1) extra space.
- In PHP, know what the built-ins actually do. array_unique keeps the first occurrence but also keeps the original keys, so wrap it in array_values to reindex. By default it compares values as strings, so 1 and "1" count as duplicates. array_count_values only accepts string and integer values. Test an empty array, a one-element array, an array where every value is the same, and mixed types.
Follow-up
- The array is already sorted. Can you do this in O(1) extra space, and how does your loop change?
- The array has a billion entries and does not fit in memory. How would you find the distinct values then (for example external sort, or hash-partitioning to disk)?
- The elements are associative arrays or objects, or strings that should match case-insensitively. What do you use as the key for the set?
Identify the heaviest tenants in a five-minute window under memory pressure
Medium · top-k · heavy hitters · streaming
The edge service handles about 3,000 requests per second across roughly 50,000 tenants, peaking near 9,000. Expose the 50 heaviest tenants by request count over the trailing five minutes so limits can be tightened before one tenant's backfill starves the fleet. You may not retain five minutes of raw records. Give the exact solution and its memory, then the bounded-memory approximation with its error stated as a formula, and say which you would ship and at what tenant cardinality that choice changes.
Approach
- Do the exact version first, because it is affordable at this cardinality: a ring of 300 one-second counters per tenant, advanced lazily, is 1,200 bytes of counters per tenant and roughly 60 to 90 MB for 50,000 tenants with overhead. Carry a running total and subtract the bucket you overwrite so a window read is O(1) rather than 300 adds.
- Extract the top 50 with a size-k min-heap over the tenant sums: O(d log k) for d tenants, against O(d log d) to sort them all. Maintaining the heap continuously instead of on query requires a tenant-to-heap-index map, because incrementing a count already inside the heap means sifting from a known position, and without that map you rebuild the heap on every request.
- State the approximation precisely rather than gesturing at sketches. Misra-Gries with m counters retains every item whose true count exceeds N/(m+1), and each retained count underestimates the truth by at most N/(m+1). With m = 1,000 and N = 900,000 requests in the window the error is roughly 900 requests, which is fine for spotting a tenant sending 50,000 and useless for ranking two tenants 200 apart.
- Say what breaks when the window slides: Misra-Gries and Space-Saving are insert-only and cannot be decremented as records age out. The workable construction is one summary per sub-window, say ten seconds, with 30 summaries merged at query time, and the merged error is the sum of the per-summary errors, so the bound degrades linearly in the number of sub-windows.
Follow-up
- The heaviest tenant is heavy because of one export job rather than user traffic. Should the limiter treat those as the same tenant?
- Two tenants sit tied at the boundary of the top 50. Does your answer flap, and does the flapping matter?
Track a rolling failure rate per destination for circuit decisions
Easy · sliding window · ring buffer · circuit breaker
The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.
Approach
- Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
- Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
- State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
- Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.
Follow-up
- The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
- A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?
Explain why the owner filter ignores the listing index
Medium · composite index · query plans · sort elimination
The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.
Approach
- Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
- For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
- Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
- Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.
Follow-up
- 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
- A dashboard runs this for 40 owners in one page load. What changes about the design?
Keep soft-deleted accounts from blocking re-registration
Easy · soft delete · unique constraints · engine differences
app_user holds user_id, tenant_id, email CITEXT, password_hash (NULL for SSO principals), email_verified_at, auth_version, status ('invited','active','suspended','deactivated'), created_at, updated_at, deleted_at. Two live accounts for one address inside a tenant must be impossible, but an address freed by a soft delete must be reusable, and the same tenant may delete and re-register it repeatedly. Write the uniqueness DDL for PostgreSQL 16, then the equivalent for MySQL 8 where partial indexes do not exist, and say what each permits once three deleted rows already hold that address.
Approach
- Start from what is actually unique: not (tenant_id, email), but (tenant_id, email) among live rows. PostgreSQL says that directly — CREATE UNIQUE INDEX app_user_live_email ON app_user (tenant_id, email) WHERE deleted_at IS NULL. A full constraint over the same two columns burns the address permanently the first time someone deletes an account.
- Keep case-insensitivity in the type or the index, never in the application: CITEXT as given, or UNIQUE (tenant_id, lower(email)) as an expression index where the extension is unavailable. A case-sensitive unique column is exactly how two accounts for one human appear.
- For MySQL 8 the predicate has to move inside the key: add a discriminator column that is a constant 0 while the row is live and is set to user_id on delete, with UNIQUE (tenant_id, email, deleted_marker). Live rows share the constant and still collide; deleted rows differ from each other and stop colliding.
- State the NULL variant and its dependency: leaving the marker NULL for deleted rows also works, because a unique index treats NULLs as distinct — true in MySQL, and true in PostgreSQL only under the default NULLS DISTINCT, which PostgreSQL 15 lets you reverse. Check the polarity against the three existing deleted rows: constant-on-live is what preserves the collision you want, and reversing it silently admits duplicate live accounts.
Follow-up
- A deleted account re-registers with the same address the next day. Do the old resource rows follow the new user_id, and how does the API keep the two principals apart?
- How do you honour an erasure request while resource_revision.actor_user_id still references this table?
Explain the difference between GET and POST methods.
Medium · Technical Fundamentals
Approach
- Begin with semantics. GET retrieves a representation and is defined as safe and idempotent: repeating it should not change server state. POST submits data for the server to process, and it is neither safe nor idempotent, so sending it twice can create two orders or two rows.
- Then explain where the data travels. GET parameters sit in the URL query string, so they show up in browser history, bookmarks, server and proxy logs and the Referer header. A GET body has no defined semantics. POST data travels in the request body, with a Content-Type such as application/x-www-form-urlencoded, multipart/form-data (needed for file uploads) or application/json.
- Correct the usual myth: POST is not encrypted. Without HTTPS, both methods are sent in plain text. POST only keeps values out of the URL and its logs, so passwords and tokens never belong in a query string. The spec sets no URL length limit, but browsers and servers enforce their own, so large payloads go in a POST body.
- Cover caching and browser behaviour. GET responses are cacheable by default and can be prefetched or replayed safely. POST responses are cached only with explicit freshness information, and refreshing a page after a POST makes the browser ask before resubmitting. The Post/Redirect/Get pattern (respond with a 303 redirect to a GET page) prevents double submissions.
- Tie it to PHP. $_GET holds the query string and $_POST holds form-encoded or multipart bodies only, so a JSON body must be read from php://input and passed to json_decode. Never let a GET endpoint change state (for example a delete link), because crawlers, link prefetchers and CSRF via an image tag can all trigger it. Protect state-changing POSTs with CSRF tokens.
Follow-up
- Where do PUT, PATCH and DELETE fit, and which of them are idempotent?
- A client sends JSON with POST, but $_POST is empty in your PHP handler. Why, and how do you read the data?
- A user double-clicks the submit button on a payment form. How do you stop the order from being created twice (for example PRG, an idempotency key, or a unique constraint)?
Describe the lifecycle of a web request from the client to the server.
Medium · Technical Fundamentals
Approach
- Client side first: the browser parses the URL, then checks its HSTS list and its caches. DNS resolution goes through the browser cache, the OS cache and the configured resolver, and the resolver may query recursively (root, then TLD, then the authoritative server) to get an IP address.
- Connection: a TCP three-way handshake (SYN, SYN-ACK, ACK), then for HTTPS a TLS handshake that agrees on keys and checks the server certificate against the hostname. TLS 1.3 needs one round trip, versus two for TLS 1.2. Keep-alive and HTTP/2 multiplexing let later requests reuse the connection instead of repeating these steps.
- The request: a method, a path, and headers such as Host, Cookie, Accept and Content-Type, plus a body for POST. On the way to the server it may pass through a CDN or a load balancer or reverse proxy, which can terminate TLS, serve the response from cache or forward it to an application server.
- Server side in a PHP stack: nginx or Apache receives the request, serves static files directly, and passes .php requests to PHP-FPM over FastCGI (or to mod_php). Each request runs a fresh PHP script lifecycle: the front controller boots the framework, the router selects a controller, middleware handles sessions and authentication, the code queries the database, and a template renders the output. All request state is discarded at the end, unless OPcache, sessions or a cache store keep it.
- Response and rendering: a status code, headers (Content-Type, Cache-Control, Set-Cookie) and a body travel back the same way. The browser parses the HTML into the DOM, fetches the CSS, JS and images it references (render-blocking CSS and synchronous scripts delay first paint), builds the CSSOM, then lays out and paints. Close by saying which stage you would measure first when a page is slow.
Follow-up
- A page loads slowly. Using the browser DevTools waterfall, how do you tell DNS, connect, TLS, server time (TTFB) and download time apart, and what would you change for each?
- Name every layer where this response could be cached, and how you would make sure a user does not see stale data after a deploy.
- What changes when the site serves HTTP/2 or HTTP/3, and why do old tricks like domain sharding and sprite sheets matter less?
One customer endpoint stalls deliveries to every other destination
Medium · bulkhead · timeouts · saturation
The egress service delivers about 1.5k webhooks/second across 40,000 destinations, with a per-destination concurrency cap of 4 and a 10-second connect-plus-read timeout. Throughput falls to 300/second, queue depth climbs, and p99 delivery latency for unaffected destinations goes from 200 ms to minutes, while the error rate barely moves. One tenant holds 900 destination rows whose URLs share a hostname that now answers in 9.5 seconds. Explain the mechanism with the arithmetic, then give the containment in the order you would apply it.
Approach
- Look at saturation before errors. A flat error rate with collapsing throughput says nothing is failing, things are waiting, so the first signal to pull is in-flight request count or pool wait time rather than the error counter. This is the distinction that decides the whole investigation.
- Group in-flight work by resolved host, not by destination id. The cap is keyed per destination row, so 900 rows sharing one hostname buy 3,600 concurrent slots against a single host, each held for 9.5 seconds. The bulkhead was never a bulkhead for that host, and grouping by the wrong dimension is why the dashboard looked healthy.
- Do the arithmetic in both directions. Required concurrency is arrival rate times latency, so 1.5k/second at 200 ms needs about 300 in flight, which is entirely consumed by 3,600 slow slots; conversely whatever concurrency is left sustains rate equals concurrency divided by 9.5 seconds, which is the 300/second you are seeing. Matching both numbers is what promotes this from a plausible story to the mechanism.
- Explain why the circuit breaker never helped. It opens on consecutive failures, and a 9.5-second response inside a 10-second timeout is a success. Slow is not failing, so an error-rate breaker cannot see this; you need a slow-call ratio, a deadline propagated from the caller's remaining budget, or a concurrency limiter.
Follow-up
- The host recovers to 80 ms. How long does the queue take to drain, and what does the drain do to the recovered host?
- Where should the 10-second timeout number actually come from?
Suggested practice checklist
The plan starts with the five reported rounds, works through each reported fundamentals question in PHP and HTTP terms, adds the debugging and security scenarios candidates mention, and ends with algorithm drills, project stories and a full mock.
Day 1 — Map the loop and inventory your stack
- Write the five reported rounds in order (Initial Contact, Skills Discussion, Practical Assessment, Technical Screens, Final Decision) and mark which ones involve code and which are conversation.
- List every language, framework and database you have used, with one project example for each, and mark whether you present as PHP or full-stack.
- Write the questions you want to ask about the process, such as what the Practical Assessment looks like.
Deliverable: A one-page loop map, a skills inventory with a project example per line, and a list of process questions.
Day 2 — Unique elements in an array, in PHP
- Write three solutions to "Find the unique elements in an array.": nested loops, sort then scan, and a hash set built with array keys using isset. Note that PHP casts array keys ("1" becomes 1, true becomes 1, null becomes "", and 1.5 is truncated to 1), so for mixed-type input use in_array with strict mode or key on serialize() of each value.
- Write down the time and space complexity of each, plus the behaviour of array_unique (string comparison by default, kept keys, array_values to reindex).
- Test every version on empty input, one element, all duplicates, mixed types such as 1 and "1", and a large array, and also solve the "occurs exactly once" reading.
Deliverable: Three working PHP functions with a complexity table and a note on key casting, a test list of edge cases, and a second function for the exactly-once reading.
Day 3 — GET versus POST, the request lifecycle and a full-stack round trip
- Write a comparison of GET and POST covering semantics, idempotency, caching, URL length, logging and CSRF, plus a short statement of why POST alone is not security.
- Write the request lifecycle as a numbered sequence from URL entry to rendered page, including DNS, TLS, the web server handing off to PHP and the database call.
- Build a small front end (plain JS fetch, React or Vue) that sends JSON to a PHP endpoint; read the body from php://input, return JSON, and write down how CORS applies and why validation must run on the server as well as the client.
- Say the GET versus POST and lifecycle answers aloud without notes, then add one slow-point example to each lifecycle stage you named.
Deliverable: A written GET vs POST comparison, a numbered request lifecycle, a working front end calling a PHP JSON endpoint with CORS and validation notes, and a recorded spoken version of both answers.
Day 4 — Database access in PHP
- Write a small PHP script that connects with PDO (exceptions on, charset set, emulated prepares off), runs a select and an insert with bound parameters, and wraps a two-step write in a transaction.
- Write what you would say about persistent connections, PHP-FPM worker counts against the database connection limit, and where credentials live.
Deliverable: A working PDO script, a short spoken answer on database connections, and a note on how an index decides query speed.
Day 5 — Debugging a slow page and web security
- Write the debugging sequence for a slow-loading page: reproduce it, read the network waterfall, time the server response, check the slowest query with EXPLAIN, then change one thing and re-measure.
- Make a checklist for common web vulnerabilities: SQL injection, XSS (htmlspecialchars on output), CSRF tokens, password_hash for passwords, and validation on the server.
Deliverable: A written slow-page debugging sequence, a security checklist with a PHP fix per item, and notes from the debugging drill.
Day 6 — Algorithms and project history
- Write a project card for two projects: architecture, your own contribution, the hardest bug and its fix, and what you would change.
- Practise explaining one technical decision to a non-technical person, such as a project manager, in plain language.
Deliverable: Two solved drills with stated complexity, two project cards, one plain-language explanation of a technical decision, and one deadline story with the debt you accepted and how you bounded it.
Day 7 — Full mock and final review
- Run a mock that goes through all four reported fundamentals questions, then a slow-page scenario and a project walkthrough, answering aloud.
- Fix the answers that were unstructured or missing an edge case and redo them once.
- Write the final questions you will ask about the team, the project mix and the next stages.
Deliverable: A completed mock with a list of revised answers and a final list of questions to ask.
Explaining your decisions and your projects
Candidates report a friendly, conversational atmosphere and technical screens that cover project history, and they suggest being able to explain why you made a technical choice to technical and non-technical listeners alike. Prepare stories where you can say what you built, why you chose it, what you measured and what happened, plus plain-language explanations of web topics.
- Take me through a project you built: the hardest technical challenge, what you did about it and what you would change now.
- Tell me about a slow page you actually fixed: how you measured it, what the cause turned out to be, and the numbers before and after.
- Tell me about a security weakness you found or prevented in a web application, and the change you made.
- Tell me about a time you explained a technical decision to a project manager or client who was not technical.
- Tell me about a time you gave a timeline for a task and it turned out to be wrong. What did you do next?
How do you handle database connections in a PHP environment?
Medium · Technical Fundamentals
Approach
- Start with the modern answer: use PDO (or mysqli with prepared statements) and build the connection once per request, in one place such as a factory or service container, not inside every function. Set the DSN with an explicit charset, turn on PDO::ERRMODE_EXCEPTION, set a default fetch mode, and set ATTR_EMULATE_PREPARES to false so the database does the parameter binding.
- Say how user input reaches the database: prepared statements with bound parameters for every value. Table names, column names and ORDER BY direction cannot be bound, so check them against an allowlist. Mention that string concatenation is the SQL injection mistake, and that the old mysql_* functions are removed from PHP.
- Describe the lifecycle: under PHP-FPM or Apache each request is share-nothing, so the connection opens during the request and closes when the script ends. PDO::ATTR_PERSISTENT keeps one connection per worker process, saving connect time, but it can carry over leftover state such as an open transaction. PHP has no pool: every PHP-FPM worker (pm.max_children) on every app server holds its own connection, so total workers can exceed the database's max_connections.
- Cover credentials and failure: load host, user and password from environment variables or config kept outside the web root and out of version control, and use a database account with only the privileges the app needs. Wrap writes that must succeed together in a transaction with a catch block that rolls back, log the PDOException, and show the visitor a generic error rather than the database message.
- Tie it to your own work: name the framework or library that configured the connection in a project you built, the query layer you used, and one problem you hit, such as too many connections or a timeout, and how you fixed it.
Follow-up
- Why would you pick PDO over mysqli, and when is mysqli still reasonable?
- What are the risks of persistent connections, and how would you decide whether to use them?
- You get a "too many connections" error under load. What do you check first, and what would you change?
Narrate an outage you owned from page to postmortem
Hard · incident response · blast radius · postmortems
Pick an incident you personally drove, ideally one where writes were affected rather than reads. In six to eight minutes: state the symptom as it first appeared on a dashboard, the blast radius you established before you knew the cause, the mitigation you applied and when, the mechanism you eventually proved, and the follow-up that would prevent a repeat. Bring numbers: error rate, tenants affected, minutes to mitigate, minutes to resolve. If you cannot name what you measured, choose a different incident.
Approach
- Open on the signal rather than the cause: which metric at which percentile moved, on which service, at what time, so the listener follows the same evidence you had rather than a conclusion you already reached.
- Separate mitigation from diagnosis out loud. State what you did to stop the bleeding (flag off, shed traffic, drain a lease, roll back a deploy) and say plainly that you did it before the mechanism was known, because those are two jobs with different deadlines.
- Establish blast radius in countable terms: how many tenants, how many writes, and crucially whether the effect was loss or only delay. An append-only revision table or a pending outbox row means the change survived and the projection was merely behind, which is a repair rather than a data-loss incident.
- Prove the mechanism instead of asserting it. Name the trace span that grew, the plan that flipped to a sequential scan, the lease that expired, plus one alternative you ruled out and the signal that stayed flat while you ruled it out.
Follow-up
- What would you do differently in the first five minutes, given the same dashboard and no more information?
- Which follow-up action did you deliberately not take, and why was dropping it the right call?
Ship under a deadline and bound the debt you chose
Medium · pagination · technical debt · tradeoffs
You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.
Approach
- Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
- Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
- Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
- Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.
Follow-up
- At what page depth does the offset version breach your latency budget, given your page size and row counts?
- What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?