I have sat on both sides of the Laravel interview — as the nervous candidate years ago, and more recently helping teams hire backend developers. The pattern is always the same: the questions that look hard on paper are rarely the ones that catch people out. What catches people out is depth on the boring fundamentals they thought they already knew.
Here are the questions I actually care about, why they matter, and how I would prepare for them.
"Walk me through what happens between a request hitting your app and the response."
This is my favourite opener because it has no trick to it — it just rewards people who understand the framework instead of memorising it. A good answer touches the public entry point, the HTTP kernel and its middleware stack, route resolution and model binding, the controller, and the response on the way back out.
The follow-up is where it gets interesting: where would you add a check that runs on every request? If the answer is "middleware" and they can explain the difference between global and route middleware, that is someone who has debugged a real request lifecycle, not just read the docs once.
The N+1 query question
Everyone says "eager loading" — that is the floor, not the ceiling. What I want to hear next is how they found the N+1 in the first place. In production you rarely see it; you feel it as a slow endpoint.
// The smell: a query inside a loop
$orders = Order::all();
foreach ($orders as $order) {
echo $order->customer->name; // one query per order
}
// The fix is obvious once you've seen it
$orders = Order::with('customer')->get();
The real answer is the tooling: Laravel Debugbar locally, Model::preventLazyLoading() in non-production so the app throws the moment a lazy load happens, and slow-query logging in production. On one platform I cut a dashboard from ~40 queries to 4 just by turning lazy-load prevention on and fixing everything it screamed about.
"How do you keep a controller thin?"
I ask this because it reveals how someone structures code under pressure, not in a tutorial. My own rule is that a controller method does three things: resolve the request (a Form Request handles validation), call one service method, and return a response. No queries, no business rules, no if ladders.
public function store(StoreOrderRequest $request, OrderService $orders)
{
$order = $orders->place($request->validated());
return new OrderResource($order);
}
When everything that does something lives in a service, your controllers become a table of contents and your logic becomes testable without booting HTTP.
Queues, and the question behind the question
"How do you send an email without blocking the response?" is really "do you understand that some work does not belong in the request?" Queue it. But the part people miss is failure: what happens when the third job in a batch throws? Can it run twice safely? I want to hear the words idempotent and retry, and ideally a story about a webhook that fired twice and a unique constraint that saved them.
Database questions that aren't about SQL trivia
I do not ask people to recite join syntax. I ask: you have a users table at ten million rows and a query is slow — what do you do? The path I am listening for is: read the actual query, run EXPLAIN, look at whether an index is used, add the right composite index, and only then consider caching. Reaching for Redis before reading the query plan is the wrong instinct, and it tells me a lot.
The question that actually separates levels: concurrency
This is the one. Two requests try to redeem the same single-use voucher in the same millisecond — what happens? The junior answer is if ($voucher->used) reject(); else markUsed();. The senior sees the gap immediately: both requests read used = false, both pass the check, both redeem. Check-then-act is not atomic.
What I want is a real defence — and ideally more than one:
// Pessimistic: hold a row lock for the length of the transaction
DB::transaction(function () use ($code) {
$voucher = Voucher::where('code', $code)->lockForUpdate()->first();
abort_if($voucher->used, 409);
$voucher->update(['used' => true]);
});
Even better is hearing that the database is the real arbiter: a unique constraint on (voucher_id, user_id) turns a race into a caught exception instead of a silent double-spend. Pessimistic locks, an optimistic version column, an atomic UPDATE ... WHERE used = false — any of them is a fine answer. The wrong answer is not knowing the race exists. Anyone who has actually shipped checkout or wallet code answers this in their sleep; anyone who hasn't usually discovers it here.
How I prepare — and what I tell people I mentor
Reading answers is not preparation. Building is. Before interviews I do three things:
- Rebuild a small slice from memory. A login flow, a paginated API endpoint with filtering, a queued job with a retry. If I can do it without the docs, I understand it. If I reach for the docs, I have found my gap.
- Re-read my own old code and explain why. Interviews love "why" questions. Why a service and not the controller? Why a job and not inline? Having the reason ready beats having the pattern memorised.
- Prepare one real war story per topic. The webhook that fired twice. The migration that locked a table in production. The query that went from 800ms to 40ms. Concrete stories beat textbook answers every time, because they prove you were actually there.
The honest truth is that senior interviews are not about knowing more facts. They are about having made enough mistakes in production that the right instinct is now automatic. You cannot fake that — but you can prepare to talk about it clearly.