Senior & Lead Engineer
Interview Mastery Hub
100 real-world, scenario-based architecture & concurrency questions designed for 5–10+ years experienced developers. Free & open for every student and engineer.
100
Real-world Q&As
100%
Scenario-Based
5-10 Yrs
Senior / Lead Depth
5 Stacks
Core Technologies
Showing 100 questions for senior interview preparation
How does Laravel Octane handle state persistence across requests, and how do you prevent catastrophic memory leaks and data bleeding in singletons?
Root Cause Analysis: Traditional PHP-FPM vs. Long-Running Octane
Under traditional PHP-FPM, the entire application lifecycle (memory, static variables, singletons) is instantiated on request arrival and wiped completely when the request terminates (shared-nothing architecture).
Under Laravel Octane (Swoole, RoadRunner, FrankenPHP), the PHP application boots once into memory. Worker processes persist across thousands of requests. Any state attached to a singleton, static property, or service provider persists across subsequent HTTP requests from completely different clients.
Critical Failure Vectors
- Data Bleeding: Storing request-scoped data (like
Auth::user()or tenant IDs) inside a singleton service constructor or static class property. Subsequent requests will read the prior user's cached identity. - Memory Leak: Appending items into static array caches (
private static array $cache = []) or listening to events without unregistering listeners.
Architectural Solution
-
Use
bindScopedInstead ofsingleton: Laravel Octane resets scoped bindings between requests:// BAD in Octane (persists across requests) $this->app->singleton(TenantManager::class, fn () => new TenantManager($request->header('X-Tenant-ID'))); // GOOD in Octane (purged after each request finishes) $this->app->scoped(TenantManager::class, fn () => new TenantManager(request()->header('X-Tenant-ID'))); -
Octane Warmers & Listeners: Flush state explicitly in
config/octane.phplisteners:OperationTerminated::class => [ FlushTemporaryCaches::class, ResetTenantContext::class, ], -
Pass Callbacks or Resolvers: Instead of caching an instantiated
$userin a service, pass aClosureor resolve dynamically viaAuth::user().
How does the Zend Engine manage memory with Zvals, Reference Counting, and the Circular Garbage Collector in PHP 8.x?
Zval Structure & Reference Counting
In PHP, variables are stored in a zval (Zend Value) struct containing:
value: Union storing the primitive or pointer.u1.type_info: Type identifier.refcount: Number of variables pointing to this compound value (arrays, objects).
The Circular Reference Trap
When Object A points to Object B, and Object B points to Object A:
$a = new Node();
$b = new Node();
$a->sibling = $b;
$b->sibling = $a;
unset($a, $b); // Refcounts decrement from 2 to 1, but never reach 0!
Even after unset(), both objects remain in memory because their refcount is 1.
Solving Daemon Memory Leaks
- PHP Cycle Collector Buffer: The GC buffer collects potential cyclic roots (10,000 max by default).
- Explicit Garbage Collection Trigger:
gc_collect_cycles(); // Forces cycle collection in loop gc_mem_caches(); // Releases cached memory chunks back to OS - Weak References (PHP 8.0+): Use
WeakReferenceso child nodes do not increment the parent's refcount:$this->parent = WeakReference::create($parentNode);
How does the InnoDB B+Tree Index work internally, and why does Leftmost Prefix Rule dictate composite index column ordering?
Why B+Tree Needs Leftmost Prefix Rule
In an InnoDB B+Tree, index keys are sorted lexicographically based on the order of columns defined in the index: (ColA, ColB, ColC).
- If a query filters on
ColBwithoutColA, the B+Tree root cannot branch becauseColBis only sorted within duplicate values ofColA. - Range Queries Stop Index Filtering: Once a range operator (
>,<,BETWEEN,>=) is evaluated, subsequent columns in the composite index cannot be used for B+Tree traversal.
Fixing the Slow Query
Because created_at used a range operator (>=), placing it first broke index lookups for status and user_id.
Optimal Composite Index Column Order:
- Exact equality matches with highest cardinality first:
user_id - Equality match:
status - Range match last:
created_at
ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at);
Query execution time drops from 8.5 seconds (full scan) to 0.8 milliseconds (index seek).
How does PostgreSQL Multi-Version Concurrency Control (MVCC) manage Tuple Visibility (`xmin`, `xmax`), and why does Table Bloat happen?
PostgreSQL MVCC Mechanics
Unlike MySQL InnoDB which overwrites data pages and creates rollback segments in Undo Logs, PostgreSQL MVCC creates a new row version (tuple) on every UPDATE and marks the old tuple as dead by recording the committing transaction ID in xmax.
- Readers never block writers, and writers never block readers.
- Table Bloat: If dead tuples are not reclaimed, physical table and index disk files continue to grow indefinitely.
Remediation:
- Tune
autovacuumparameters for high-churn tables:ALTER TABLE orders SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_cost_limit = 2000 ); - Reclaim bloated disk space online without locking reads/writes via
pg_repack.
How does the Redis Redlock Algorithm provide Fault-Tolerant Distributed Locking across independent Redis nodes?
Why a Single Redis Master is Insufficient for Strict Distributed Locking
In a standard Master-Replica Redis setup, replication is asynchronous. If the master crashes immediately after granting a lock before replicating to replicas, the promoted replica can grant the exact same lock to a second client, causing catastrophic double-execution.
The Redlock Algorithm Workflow
- Client initializes $N$ independent Redis master nodes (e.g. $N=5$).
- Client gets current millisecond timestamp $T_1$.
- Client attempts to acquire the lock sequentially across all $N$ instances using identical key, random unique value, and TTL (e.g. 10s) with a short timeout (e.g. 50ms) per node.
- Client computes elapsed time $Elapsed = T_2 - T_1$.
- Lock Acquisition Success: If the client acquired the lock on majority of nodes (>= 3 out of 5) AND $Elapsed < TTL$, the lock is valid for $ValidityTime = TTL - Elapsed$.
- Failure & Rollback: If client failed to acquire on majority, it immediately sends an unlock script (
Lua EVAL) to all instances, even those where it thought acquisition failed.
How do you prevent Deadlocks when processing concurrent financial transactions or inventory deductions across multiple queue workers in Laravel?
Why Deadlocks Occur in Concurrent Workers
A deadlock occurs when Worker A holds a lock on Resource 1 and requests Resource 2, while Worker B holds a lock on Resource 2 and requests Resource 1. Neither can proceed.
Battle-Tested Remediation Strategy
-
Strict Ordering of Locks: Always sort entity IDs before acquiring database row locks (
lockForUpdate):// Sort items by ID so all workers acquire locks in the exact same sequence $itemIds = collect($cartItems)->pluck('product_id')->sort()->values(); DB::transaction(function () use ($itemIds) { $products = Product::whereIn('id', $itemIds)->lockForUpdate()->get(); // Deduct inventory safely }, 5); // 5 retries on deadlock -
Atomic SQL Decrements: Instead of
SELECT -> calculate in PHP -> UPDATE, perform atomic database decrements:$affected = DB::table('products') ->where('id', $productId) ->where('stock', '>=', $quantity) ->decrement('stock', $quantity); if ($affected === 0) { throw new OutOfStockException("Insufficient inventory."); } -
Redis Atomic Distributed Lock (Redlock / Mutex): Prevent queue workers from touching the same wallet simultaneously:
use Illuminate\Support\Facades\Redis; Redis::throttle("wallet-lock:{$userId}") ->block(10) ->allow(1) ->every(1) ->then(function () use ($userId, $amount) { // Execute transaction safely });
Explain PHP 8.4 Asymmetric Visibility, Property Hooks, and Constructor Property Promotion.
1. Asymmetric Visibility (PHP 8.4)
Allows a property to be publicly readable but privately or protectedly writable:
class User
{
public private(set) string $status = 'pending';
public function activate(): void
{
$this->status = 'active'; // Allowed inside class
}
}
$user = new User();
echo $user->status; // Allowed: readable
// $user->status = 'banned'; // FATAL ERROR: cannot modify private(set) property outside
2. Property Hooks (PHP 8.4)
Replaces repetitive getters and setters with direct property hooks:
class Product
{
public string $title;
public string $slug {
get => Str::slug($this->title);
}
public float $price {
set (float $value) {
if ($value < 0) throw new InvalidArgumentException("Price cannot be negative");
$this->price = round($value, 2);
}
}
}
Explain InnoDB Locking Modes: Record Locks, Gap Locks, and Next-Key Locks, and how they prevent Phantom Reads.
The Three InnoDB Row Lock Types
- Record Lock: Locks the physical index record itself (e.g.
WHERE id = 5 FOR UPDATE). - Gap Lock: Locks the gap between index records (or before the first / after the last) to prevent other transactions from inserting new rows in that gap.
- Next-Key Lock: A combination of a Record Lock on the index record plus a Gap Lock on the gap preceding it.
Why MySQL Uses Next-Key Locks by Default
Under the default REPEATABLE READ isolation level, MySQL uses Next-Key locking to solve the Phantom Read problem (preventing another transaction from inserting a new row that matches your query's range before you commit).
Reducing Lock Contention:
Switching transaction isolation to READ COMMITTED (with binlog_format=ROW) disables Gap Locks except for foreign key constraint checks and duplicate key checks.
How do GIN (Generalized Inverted Index) and GiST indexes optimize high-speed JSONB queries and Full-Text Search in PostgreSQL?
GIN with jsonb_path_ops
Standard GIN indexes every key and value. For direct JSON path containment queries (@>), jsonb_path_ops produces an index that is 3x smaller and faster:
CREATE INDEX idx_user_metadata_gin ON users USING GIN (metadata jsonb_path_ops);
-- Optimized query uses GIN index seek
SELECT * FROM users
WHERE metadata @> '{"subscription": {"plan": "enterprise", "status": "active"}}';
Execution time drops from 6.8s to 1.2ms.
Explain the 3 High-Scale Cache Failure Patterns: Cache Avalanche, Cache Breakdown (Dog-Piling), and Cache Penetration, with mitigations.
1. Cache Avalanche (Mass Simultaneous Expiration)
- Problem: Millions of keys expire at the same instant; massive traffic rushes to the DB.
- Solution: Add Random TTL Jitter:
$ttl = 3600 + rand(0, 600); // 60 to 70 minutes randomized expiration
2. Cache Breakdown / Dog-Piling (Hot Key Expiry)
- Problem: A single viral product key expires; 20,000 concurrent requests miss simultaneously.
- Solution: Distributed Mutex Lock or Background Pre-warming / Stale-While-Revalidate.
3. Cache Penetration (Non-Existent Keys Attack)
- Problem: Malicious requests query non-existent IDs (
id = -9999), bypassing cache and hitting DB on every request. - Solution:
- Cache Empty Objects / Nulls with short TTL (e.g. 60 seconds).
- Bloom Filter: Use Redis Bloom filter to determine in $O(1)$ whether an ID could possibly exist before querying database.
How does the Laravel Service Container resolve dependencies using Reflection, and what are the performance trade-offs in large applications?
How Container Resolution Works
- When you request a class (e.g.
app(PaymentService::class)), if no binding exists, the container checksisBuildable($concrete). - It instantiates
ReflectionClass($concrete). - It inspects
getConstructor()and iterates through parameters viaReflectionParameter. - It recursively resolves parameter typehints from the container or defaults until the dependency graph is fully satisfied.
Optimization Strategies for Large Applications
- Service Provider Deferral: Use
\Illuminate\Contracts\Support\DeferrableProviderso large service providers are only loaded when their bound service is explicitly requested. - Route & Config Caching:
php artisan route:cache,php artisan config:cacheavoids repetitive reflection scans on routes and controllers. - Explicit Bindings: Direct closures (
$this->app->bind(Service::class, fn () => new Service(...))) skip reflection overhead entirely.
How does the JIT (Just-In-Time) compiler in PHP 8.x work, and why does it benefit CPU-heavy tasks but not typical database-heavy web apps?
How PHP JIT Compiles Code
- Traditional PHP with OPcache: PHP source code is parsed into Abstract Syntax Tree (AST) -> Compiled into Opcodes -> Executed by the Zend VM bytecode interpreter.
- JIT (Tracing / Function JIT): Identifies frequently executed opcode paths ("hot spots") and translates those opcodes directly into raw machine code (x86_64 or ARM CPU instructions), bypassing Zend VM.
Why Web Apps See Minimal Gains
- Typical web applications are I/O-bound (waiting on MySQL network queries, Redis reads, filesystem, external HTTP calls).
- JIT accelerates CPU-bound mathematical computation, image rendering, 3D matrix math, and machine learning tokenization, where PHP spends 90%+ time in pure CPU instructions.
How do you optimize deep pagination (`SELECT * ... LIMIT 5000000, 20`) on large tables with 100M rows?
Why LIMIT 5000000, 20 is Slow
MySQL must scan, read from disk, and discard all 5,000,000 preceding rows before returning the 20 requested records.
Solution 1: Cursor-Based Pagination (Seek Method) - FASTEST
Instead of offset, filter by the last seen ID:
SELECT * FROM audit_logs
WHERE id < 45000000
ORDER BY id DESC
LIMIT 20;
Direct B+Tree seek, runs in 0.4 milliseconds on any page number.
Solution 2: Deferred Join (Late Row Lookup)
If random page offset jumps are required, use a covering index subquery:
SELECT a.* FROM audit_logs a
JOIN (
SELECT id FROM audit_logs
ORDER BY created_at DESC
LIMIT 5000000, 20
) AS sub ON a.id = sub.id;
The inner subquery scans only the memory-efficient index tree without fetching heavy row columns until the final 20 IDs are found.
How does PgBouncer Connection Pooling operate in Transaction Mode vs. Session Mode, and what PostgreSQL features break in Transaction Mode?
PgBouncer Pool Modes Comparison
- Session Mode: A client holds a dedicated backend PostgreSQL connection for the entire lifetime of its socket.
- Transaction Mode: A backend PostgreSQL connection is assigned to a client ONLY for the duration of a single transaction and returned to the pool immediately upon
COMMITorROLLBACK.
Features Incompatible with Transaction Mode:
- Session-level Prepared Statements (unless using PgBouncer 1.21+ with named prepared statement support).
SET SESSION/LISTEN/NOTIFY.- Temporary Tables (they disappear when connection changes).
DISCARD ALL/DEALLOCATE ALL.
Compare Rate Limiting Algorithms: Token Bucket, Leaky Bucket, Fixed Window Counter, and Sliding Window Log/Counter.
Comparison Matrix
| Algorithm | Pros | Cons | Ideal For |
|---|---|---|---|
| Token Bucket | Allows bursts up to bucket capacity, smooth refills | Memory state tracking required | API Gateways (Stripe / GitHub style) |
| Leaky Bucket | Guarantees constant, smooth output rate | Discards bursts if queue full | Egress traffic shaping |
| Fixed Window | Low memory | Boundary spikes (2x burst at window edges) | Basic IP throttling |
| Sliding Window Counter | Prevents boundary spikes, memory efficient | Approximate calculation | High-scale tier limiting |
Sliding Window Lua Script in Redis
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
-- Remove timestamps older than current sliding window
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
local current_count = redis.call('ZCARD', key)
if current_count < limit then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1 -- Allowed
else
return 0 -- Blocked (HTTP 429)
end
How do you execute Zero-Downtime database migrations on tables with 50+ million rows without locking writes in production Laravel?
Root Cause
Standard ALTER TABLE statements in MySQL can acquire exclusive metadata locks (MDL), blocking all incoming INSERT/UPDATE operations until the table rewrite finishes.
Safe Migration Patterns
- Online DDL with Algorithm=INPLACE, Lock=NONE:
ALTER TABLE orders ADD COLUMN fulfillment_status VARCHAR(30) DEFAULT 'pending', ALGORITHM=INPLACE, LOCK=NONE; - The Expand/Contract (Blue-Green Schema) Pattern:
- Step 1 (Expand): Add the new column as
NULLABLE(instant operation). - Step 2 (Dual-Write): Write to both old and new columns via model events or repository layer.
- Step 3 (Backfill in Background Queues): Chunk existing 60M rows with a queue worker in batches of 5,000 using ID-based cursor pagination.
- Step 4 (Switch Reads): Deploy code pointing all read queries to the new column.
- Step 5 (Contract): Drop the old column after zero dependencies remain.
- Step 1 (Expand): Add the new column as
- Use GitHub's
gh-ostor Perconapt-online-schema-change: Creates an ghost shadow table, triggers/binlog replays changes, and performs an atomic table swap without holding locks.
How do PHP Fibers work under the hood, and how do they differ from full async threads (Go Goroutines / Node.js Event Loop)?
Fibers: Full-Stack Stackful Coroutines
A Fiber in PHP 8.1+ represents an interruptible function with its own call stack:
- Can be paused (
Fiber::suspend()) and resumed ($fiber->resume()) from anywhere in the call hierarchy. - Cooperative Multitasking: Unlike OS threads, Fibers do NOT run in parallel across CPU cores. They yield execution cooperatively.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('paused state');
echo "Resumed with: {$value}\n";
});
$result = $fiber->start(); // Returns 'paused state'
$fiber->resume('new value'); // Outputs: Resumed with: new value
Framework Ecosystem
Fibers provide low-level primitives utilized by async engines like Revolt, Amphp v3, and ReactPHP to eliminate "callback hell" while keeping synchronous-looking code.
How do you interpret MySQL `EXPLAIN ANALYZE` output to diagnose `Using filesort` and `Using temporary`?
Diagnosing EXPLAIN ANALYZE
Using filesort: MySQL cannot use an index to satisfy theORDER BYclause and must perform an extra sorting pass in memory (sort_buffer_size) or on disk.Using temporary: MySQL creates an internal temporary table (in RAM or disk) to hold intermediate results, common withGROUP BYandDISTINCTon non-indexed columns.
Remediation:
- Create a composite index covering both the filter and the order:
CREATE INDEX idx_merchant_created ON orders (merchant_id, created_at DESC); - When query runs
WHERE merchant_id = 5 ORDER BY created_at DESC, MySQL reads pre-sorted index nodes directly withoutfilesortor temporary tables.
When should you use BRIN (Block Range Index) over B-Tree indexes for massive Time-Series and Log tables?
Why BRIN Indexes Are Revolutionary for Append-Only Data
For data inserted chronologically (where physical disk block ordering matches column value order), a BRIN index stores only the minimum and maximum value for each physical page range (e.g. 128 pages):
- Index Size: BRIN index consumes only 50MB compared to 22GB for B-Tree (a 99.7% reduction).
CREATE INDEX idx_logs_created_brin ON event_logs USING BRIN (created_at);
How do you design a Real-Time Notification System delivering to 10M concurrent users with WebSockets and Redis Pub/Sub?
Architectural Blueprint
[Client Socket] ---> [Load Balancer / Nginx] ---> [WebSocket Gateway Cluster (Node/Go/Reverb)]
^
| Redis Pub/Sub Cluster / Kafka
|
[Notification Service]
Core Architecture Components:
- Stateful Connection Nodes: Gateway instances maintain persistent TCP socket connections and keep a local in-memory lookup map of
user_id -> [socket_fd_1, socket_fd_2]. - Channel Partitioning in Redis Pub/Sub: Broadcast messages are partitioned by tenant or topic channels (
broadcast:globalvsuser:12345). - Backpressure & Drop Policies: If a client's TCP socket buffer is saturated, drop non-critical status updates to prevent memory exhaustion on the gateway node.