Laravel Interview Questions for Experienced Developers

Laravel Interview Questions and Answers for 5 Year Experience

If you have five years of Laravel experience, you have already built real applications. The interview room knows that. What they’re probing now isn’t whether you can generate a migration or write a route, it’s whether you understand why things work the way they do, and whether you’ve hit real world problems and solved them thoughtfully.

This guide covers the advanced Laravel interview questions most commonly asked at the senior/lead level, with concise answers that reflect how an experienced developer actually thinks.

Advanced Laravel Interview Questions and Answers for 5 Year Experience

Core Architecture

1) What is the Laravel service container and how does it actually work?

The service container is Laravel’s dependency injection (DI) system. When you type hint a dependency in a controller constructor, the container introspects the type hint using PHP reflection and resolves an instance automatically.

Under the hood, Illuminate\Container\Container uses ReflectionClass to examine constructor parameters. If a binding exists, it uses that. If not, it attempts to auto resolve the concrete class. This is called auto-wiring.

You explicitly bind classes in a service provider’s register() method:

// Bind an interface to a concrete implementation
$this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);

// Singleton: same instance returned every time
$this->app->singleton(CacheManager::class, fn($app) => new CacheManager($app));

The container’s make() method resolves instances. resolve() is an alias. Facade static calls ultimately delegate to the container as well.

What interviewers are listening for: Understanding that the container is not magic — it’s PHP reflection + a registry of bindings. Candidates who have only used it without understanding the resolution chain often stumble here.

2) What is the difference between register() and boot() in a service provider?

register() is called during the early bootstrapping phase, before all service providers are registered. You should only bind things into the container here. You must not call $this->app->make() or use other services inside register() because those services may not exist yet.

boot() is called after all service providers have been registered. Here it’s safe to use other services, define view composers, register event listeners, publish assets, or extend existing bindings.

public function register(): void
{
    // Only bindings here
    $this->app->singleton(InvoiceService::class);
}

public function boot(): void
{
    // Safe to use resolved services here
    View::composer('layouts.app', NavComposer::class);
    
    Gate::define('edit-post', fn(User $user, Post $post) => $user->id === $post->user_id);
}

3) What is a facade and how does it work internally?

A facade provides a static style interface to a class that lives in the service container. The trick is the __callStatic magic method on the base Illuminate\Support\Facades\Facade class.

When you call Cache::get('key'), PHP calls __callStatic('get', ['key']) on the Cache facade. That method resolves the underlying bound class from the container and calls get on it dynamically.

The getFacadeAccessor() method on each facade returns the container binding key:

class Cache extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'cache';
    }
}

Why this matters in testing: Because facades proxy to container bindings, you can swap them in tests using Cache::fake() or Cache::shouldReceive() without changing application code.

4) Explain the difference between contracts and facades.

Contracts are PHP interfaces defined in the Illuminate\Contracts namespace. They define what a service must do without specifying how.

Facades are a static access layer to services. They are convenient but hide dependencies making it harder to see what a class actually needs.

Contracts are preferable when you want to type-hint dependencies explicitly (clear, testable) or swap implementations. Facades are fine for rapid development but should be used thoughtfully in large codebases.

// Using a contract (explicit, testable)
public function __construct(
    private CacheContract $cache,
    private QueueContract $queue
) {}

// Using a facade (implicit dependency)
public function handle(): void
{
    Cache::put('key', 'value', 3600);
}

Eloquent ORM

5) Explain the N+1 query problem and how to solve it.

N+1 happens when you load a collection and then access a relationship inside a loop, causing one query per record instead of a single query for all related records.

// N+1: fires 1 query for posts + N queries for each author
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name; // separate query each time
}

// Fixed with eager loading
$posts = Post::with('author')->get(); // 2 queries total

For nested relationships: Post::with('author.profile')->get(). For conditional eager loading: Post::with(['comments' => fn($q) => $q->where('approved', true)])->get().

Laravel Debugbar or DB::getQueryLog() are the standard tools to catch N+1 issues in development.

6) What are accessors and mutators, and how have they changed in Laravel 9+?

Accessors let you transform an attribute when you retrieve it. Mutators transform it when you set it.

Pre-Laravel 9 syntax:

// Accessor
public function getFullNameAttribute(): string
{
    return "{$this->first_name} {$this->last_name}";
}

// Mutator
public function setPasswordAttribute(string $value): void
{
    $this->attributes['password'] = bcrypt($value);
}

Laravel 9+ syntax using Attribute cast (preferred):

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function fullName(): Attribute
{
    return Attribute::make(
        get: fn() => "{$this->first_name} {$this->last_name}",
    );
}

protected function password(): Attribute
{
    return Attribute::make(
        set: fn(string $value) => bcrypt($value),
    );
}

The new syntax is cleaner and keeps accessor/mutator pairs together.

7) What are Eloquent scopes and when would you use them?

Scopes let you extract reusable query logic into named methods on the model.

Local scope — called explicitly:

// Model definition
public function scopeActive(Builder $query): Builder
{
    return $query->where('status', 'active');
}

// Usage
User::active()->get();
User::active()->where('country', 'IN')->paginate(20);

Global scope – applied automatically to every query for that model. Useful for multi-tenancy or soft deletes (which is implemented this way internally):

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('tenant_id', auth()->user()->tenant_id);
    }
}

// In model boot or using attribute
protected static function booted(): void
{
    static::addGlobalScope(new TenantScope());
}

Remove a global scope for a specific query with withoutGlobalScope(TenantScope::class).

8) What is the difference between hasOne, hasMany, belongsTo, and belongsToMany?

RelationshipForeign Key LocationUse Case
hasOneOn the related tableUser has one Profile
hasManyOn the related tableUser has many Posts
belongsToOn the current tablePost belongs to User
belongsToManyPivot tableUser belongs to many Roles

belongsTo is the inverse of hasOne / hasMany. The model defining belongsTo holds the foreign key column.

For belongsToMany, Laravel expects a pivot table named alphabetically (e.g., role_user). You can customize it with the second parameter: belongsToMany(Role::class, 'user_roles').

9) How does soft delete work internally in Eloquent?

Soft deletes use the SoftDeletes trait, which registers a global scope (SoftDeletingScope) on the model. This scope automatically appends WHERE deleted_at IS NULL to every query, filtering out soft-deleted records without any effort from the developer.

When you call $model->delete(), Eloquent sets deleted_at to the current timestamp rather than running a DELETE SQL statement.

To include soft-deleted records: Model::withTrashed()->get(). To query only deleted records: Model::onlyTrashed()->get(). To permanently delete: $model->forceDelete().

Read our detailed guide on: Laravel 12 REST API with Sanctum

Routing and Middleware

10) What is route model binding and how does it work?

Route model binding automatically injects a model instance into your route or controller based on a route parameter. Laravel resolves the model using the parameter name by convention.

// Laravel resolves User by the {user} segment automatically
Route::get('/users/{user}', [UserController::class, 'show']);

public function show(User $user): Response
{
    return response()->json($user);
}

By default, it resolves using the model’s primary key. To resolve by a different column, override getRouteKeyName() on the model:

public function getRouteKeyName(): string
{
    return 'slug';
}

For custom resolution logic without changing the model, use explicit binding in RouteServiceProvider:

Route::bind('user', fn(string $value) => User::where('uuid', $va

11) How do you create and register middleware?

php artisan make:middleware EnsureUserIsVerified
public function handle(Request $request, Closure $next): Response
{
    if (!$request->user()?->hasVerifiedEmail()) {
        return redirect()->route('verification.notice');
    }

    return $next($request);
}

Register it in bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php (Laravel 10 and below):

// Laravel 11+
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias(['verified.email' => EnsureUserIsVerified::class]);
})

Apply to routes with ->middleware('verified.email').

Terminable middleware can do work after the response is sent by implementing a terminate() method — useful for logging.

Check our tutorial on Explained Laravel Routing

Queues and Jobs

12) How does the Laravel queue system work?

Queues let you defer time-consuming work (emails, notifications, report generation) to a background worker process, so the HTTP response returns immediately.

A job class serializes itself into the queue backend (database, Redis, SQS, Beanstalkd). A queue:work process picks jobs off the queue and executes them.

// Dispatch a job
ProcessInvoice::dispatch($invoice)->onQueue('billing')->delay(now()->addMinutes(5));

// Job class
class ProcessInvoice implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public Invoice $invoice) {}

    public function handle(InvoiceService $service): void
    {
        $service->process($this->invoice);
    }
}

Key properties to know:

  • $tries – max attempts before marking as failed
  • $timeout – seconds before the job is killed
  • $backoff – seconds to wait before retry
  • $maxExceptions – fail after this many uncaught exceptions

13) What is the difference between dispatch() and dispatchSync()?

dispatch() pushes the job onto the configured queue and returns immediately. The job runs asynchronously in a worker.

dispatchSync() (formerly dispatchNow()) runs the job immediately in the current process, bypassing the queue entirely. Useful for testing or situations where you need the job result before continuing.

14) How do you handle failed jobs?

Failed jobs are stored in the failed_jobs table when they exhaust their retry attempts. You configure failure handling on the job itself:

public function failed(Throwable $exception): void
{
    // Notify, alert, or compensate
    Notification::route('mail', '[email protected]')
        ->notify(new JobFailedNotification($exception));
}

Commands to manage failed jobs:

  • php artisan queue:failed – list failed jobs
  • php artisan queue:retry {id} – retry a specific job
  • php artisan queue:retry all – retry all failed jobs
  • php artisan queue:flush – delete all failed jobs

Caching

15) How does Laravel’s caching system work?

The Cache facade proxies to the CacheManager, which resolves the configured driver (file, database, Redis, Memcached, DynamoDB, array). All drivers implement Illuminate\Contracts\Cache\Store.

// Store for 10 minutes
Cache::put('user:42', $user, now()->addMinutes(10));

// Retrieve or default
$user = Cache::get('user:42', fn() => User::find(42));

// Remember pattern — retrieve or compute and store
$posts = Cache::remember('homepage_posts', 3600, fn() => Post::published()->latest()->take(10)->get());

// Forget
Cache::forget('user:42');

// Cache tags (only Redis/Memcached)
Cache::tags(['posts', 'user:42'])->put('post_list', $posts, 3600);
Cache::tags(['posts'])->flush(); // invalidates all tagged items

In production: Use Redis rather than the file or database driver. File cache does not scale across multiple servers; Redis is shared and fast.

Authentication and Authorization

15) What is the difference between authentication and authorization in Laravel?

Authentication verifies who a user is (login, token validation). Authorization determines what that user is allowed to do.

Laravel uses Gates for closure-based authorization and Policies for model centric authorization.

// Gate
Gate::define('update-post', fn(User $user, Post $post) => $user->id === $post->user_id);

// Check in controller
$this->authorize('update-post', $post);

// Policy (generated via: php artisan make:policy PostPolicy --model=Post)
public function update(User $user, Post $post): bool
{
    return $user->id === $post->user_id;
}

Policies are preferred over gates for anything model-related because they keep authorization logic organized and discoverable.

16) How does Laravel Sanctum differ from Passport?

SanctumPassport
ProtocolCookie-based sessions + simple tokensFull OAuth 2.0
Use caseSPAs, mobile apps, simple APIsThird-party OAuth clients
Setup complexitySimpleMore involved
Token typePersonal access tokens, session cookiesOAuth access/refresh tokens
Best forFirst-party clientsPublic API with delegated access

Use Sanctum for your own SPA or mobile app consuming your API. Use Passport when you need to act as an OAuth 2.0 provider – for example, allowing third-party apps to authenticate via your platform.

Design Patterns and Architecture

17) What design patterns does Laravel use internally?

Laravel’s codebase is a good example of several classic patterns:

  • Facade – static proxy to container-resolved instances
  • Repository – not enforced by Laravel, but commonly implemented on top of Eloquent
  • Observer – Eloquent model events (created, updated, deleted)
  • Decorator – middleware pipeline wraps each request in successive layers
  • Factory – model factories for testing; FormRequest factories
  • Strategy – queue drivers, cache drivers, mail drivers are interchangeable strategies
  • Template methodartisan make:command base class with a handle() hook
  • Singleton – service container singleton() bindings

18) What is the Repository pattern and should you use it with Laravel?

The Repository pattern adds an abstraction layer between your business logic and your data access logic. Instead of calling Eloquent directly in your service classes, you call a repository interface.

interface UserRepositoryInterface
{
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function create(array $data): User;
}

class EloquentUserRepository implements UserRepositoryInterface
{
    public function findById(int $id): ?User
    {
        return User::find($id);
    }
    // ...
}

The honest answer in an interview: Laravel’s Eloquent already provides a clean and tested data layer. Adding a repository on top can be justified when you genuinely need to swap data sources (e.g., switching from Eloquent to an API client) or when unit-testing business logic without hitting the database. But in most Laravel applications, it’s an extra abstraction that adds overhead without a clear benefit. A Service class that uses Eloquent directly is often sufficient.

19) How do events and listeners work in Laravel?

Events decouple application components. When something significant happens, you fire an event; one or more listeners respond independently.

// Fire an event
event(new OrderPlaced($order));

// Or
OrderPlaced::dispatch($order);

// Listener
class SendOrderConfirmation implements ShouldQueue
{
    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->order->user)->send(new OrderConfirmationMail($event->order));
    }
}

Register in EventServiceProvider (Laravel 10) or use the #[ListensTo] attribute (Laravel 11+):

protected $listen = [
    OrderPlaced::class => [
        SendOrderConfirmation::class,
        UpdateInventory::class,
        NotifyWarehouse::class,
    ],
];

Make listeners implement ShouldQueue so they run asynchronously without blocking the request.

Performance and Optimization

20) What are some common ways to optimize a slow Laravel application?

This is a broad question. A strong answer covers multiple layers:

Database:

  • Eager load relationships (fix N+1)
  • Add database indexes on columns used in WHERE, ORDER BY, and JOIN
  • Use select() to limit columns fetched
  • Use chunk() or cursor() for large datasets instead of all()

Caching:

  • Cache expensive queries with Cache::remember()
  • Use Redis for session and cache drivers
  • Use HTTP caching headers for public responses

Application:

  • Run php artisan optimize in production (caches config, routes, views)
  • Use queues for any work not needed in the current HTTP response
  • Use lazy collections for memory-efficient processing

Infrastructure:

  • Use PHP OPcache in production
  • Use a CDN for static assets
  • Horizontal scaling with queue workers

21) What is the difference between chunk() and cursor() when processing large datasets?

Both avoid loading thousands of records into memory at once, but they work differently.

chunk() runs multiple paginated SELECT queries, loading records in batches. Each batch is a full Eloquent collection:

User::where('subscribed', true)->chunk(500, function (Collection $users) {
    foreach ($users as $user) {
        // process
    }
});

cursor() uses a PHP generator and a single query with a server-side cursor, streaming one record at a time. Memory usage is much lower, but each record is still a full Eloquent model:

foreach (User::where('subscribed', true)->cursor() as $user) {
    // process one at a time
}

Use chunk() when you need to perform batch operations. Use cursor() when memory is a hard constraint and you’re processing one record at a time.

Testing

22) What is the difference between unit tests and feature tests in Laravel?

Unit tests test a single class or method in isolation, with all dependencies mocked. They don’t boot the Laravel application and are extremely fast.

Feature tests test a full HTTP request/response cycle or an integration of multiple components. They boot the application and can interact with the database, routes, middleware, and more.

// Feature test
public function test_authenticated_user_can_create_post(): void
{
    $user = User::factory()->create();

    $response = $this->actingAs($user)
        ->postJson('/api/posts', ['title' => 'Test', 'body' => 'Content']);

    $response->assertCreated();
    $this->assertDatabaseHas('posts', ['title' => 'Test', 'user_id' => $user->id]);
}

For a senior, the expectation is that you write both — unit tests for business logic, feature tests for API contracts and full flows.

23) How do you test a queued job without actually running a queue worker?

Use Queue::fake() at the top of your test. It intercepts dispatch() calls and allows assertions without ever running the job:

public function test_order_dispatches_invoice_job(): void
{
    Queue::fake();

    $order = Order::factory()->create();
    
    (new PlaceOrderAction())->execute($order);

    Queue::assertPushed(ProcessInvoice::class, fn($job) => $job->order->id === $order->id);
    Queue::assertNotPushed(NotifyAdmin::class); // assert something was NOT queued
}

Similarly: Mail::fake(), Event::fake(), Notification::fake(), Storage::fake().

Miscellaneous Advanced Topics

24) What is the Laravel pipeline and how can you use it?

The Pipeline class lets you pass an object through a series of stages (pipes), where each stage can modify it and pass it along. Middleware is implemented using the pipeline pattern.

You can use it in your own code for multi-step processing:

$result = app(Pipeline::class)
    ->send($order)
    ->through([
        ValidateInventory::class,
        ApplyDiscounts::class,
        CalculateTax::class,
    ])
    ->thenReturn();

Each pipe class implements a handle($payload, Closure $next) method. This is a clean alternative to deeply nested conditionals.

25) What is the difference between config(), env(), and where should each be used?

env() reads directly from the .env file. It should only be used inside config/ files, never directly in application code.

config() reads from the cached config files, which are loaded once and then served from memory (or from the cached bootstrap file in production after php artisan config:cache).

If you call env() in application code and run php artisan config:cache, the .env file is no longer read at runtime, and your env() calls will return null. This is a common production bug.

// Correct: in config/services.php
'stripe_key' => env('STRIPE_SECRET_KEY'),

// Correct: in application code
$key = config('services.stripe_key');

// Wrong: in application code (breaks after config:cache)
$key = env('STRIPE_SECRET_KEY');

26) How does Laravel handle broadcasting and real-time events?

Broadcasting allows server-side events to be pushed to the client in real time using WebSockets. Laravel fires an event that implements ShouldBroadcast, and the broadcasting driver (Pusher, Ably, Reverb, or a self-hosted solution) delivers it to subscribed clients.

class OrderStatusUpdated implements ShouldBroadcast
{
    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel("orders.{$this->order->id}")];
    }
}

On the frontend, Laravel Echo subscribes to the channel:

Echo.private(`orders.${orderId}`)
    .listen('OrderStatusUpdated', (e) => {
        console.log(e.order.status);
    });

Laravel Reverb (introduced in Laravel 11) is the official first-party WebSocket server, removing the dependency on Pusher for many use cases.

Troubleshooting Questions

27) Your application is slowing down under load. What is your debugging process?

A structured answer impresses more than a list of guesses:

  1. Check logs firststorage/logs/laravel.log, server error logs, Sentry/Bugsnag if configured
  2. Enable query loggingDB::enableQueryLog() and DB::getQueryLog() to spot slow or repeated queries
  3. Use Debugbar locally – Laravel Debugbar shows query counts, time, memory usage, and cache hits per request
  4. Profile with Telescope – in staging, Laravel Telescope records every request, query, job, and mail
  5. Check for N+1 – look for repeated queries with incrementing IDs (sign of N+1)
  6. Check indexes – run EXPLAIN SELECT ... on slow queries
  7. Check queue length – if queued work is backed up, workers may be under-provisioned
  8. Review cache hit rate – if Redis is misconfigured, expected cache hits may be misses
  9. Check server metrics – CPU, memory, and I/O on the server or container

28) What are some common causes of memory exhaustion in a Laravel application?

  • Loading large Eloquent collections with all() or get() without pagination
  • Processing large files without streaming
  • Accumulating queries inside loops without clearing
  • Memory leaks in long-running queue workers (a worker process accumulates state across many jobs — restart workers periodically with --max-jobs or Supervisor)
  • Eager loading too broadly (loading entire relationship trees when only one column is needed — use select() or pluck())

Conclusion

At five years of Laravel experience, interviewers want to see that you understand the framework deeply enough to make architectural decisions, not just use it. The questions in this guide reflect what actually comes up at that level service container internals, performance under load, queue architecture, testing strategy, and the trade-offs between different approaches.

Similar Posts