Clean Architecture in Laravel Real Example

Khimananda Oli 7 min read Web Development
Clean Architecture in Laravel Real Example

By Khimananda Oli | Last reviewed: August 2026

Most Laravel applications start simple but degrade into tightly coupled controllers and models as business logic grows. Applying clean architecture in Laravel real example patterns reverses this entropy by placing domain rules at the center and pushing frameworks, databases, and HTTP to the periphery. This guide walks through a production-grade implementation that keeps your core logic portable, testable, and independent of Eloquent or specific queue drivers.

What is clean architecture in Laravel real example structure?

Clean architecture enforces a strict dependency rule: outer layers depend on inner layers, never the reverse. In a Laravel context, this means your controllers (Interface Adapters) call application services (Use Cases), which orchestrate domain entities and repository interfaces. The infrastructure layer implements those interfaces using Eloquent, Redis, or external APIs. This inversion isolates business policy from technical details.

DomainEntities & Value ObjectsApplicationUse Cases / ServicesInfrastructureEloquent, Queues, APIsInterfaceControllers, Commands
Clean architecture in Laravel real example: dependencies flow inward from Interface and Infrastructure toward the Domain core

The directory structure typically mirrors these boundaries. Instead of grouping by technical concern (Models, Controllers), you group by feature or bounded context. A common mistake is creating an "Application" folder that becomes a dumping ground; keep use cases focused on single business operations. For teams transitioning from standard MVC, this shift feels verbose initially but pays dividends when requirements change. If you are also managing complex data stores, understanding MongoDB administration basics helps inform how repository abstractions should handle document versus relational paradigms.

How do you implement domain entities and value objects in Laravel?

Domain entities represent business concepts with identity, while value objects describe attributes without identity. Both must be free of Laravel facades, Eloquent traits, or validation helpers. They enforce invariants through constructors and methods, ensuring invalid states cannot exist.

Create pure PHP entities

<?php
declare(strict_types=1);

namespace App\Domain\Order;

final class Order
{
    private function __construct(
        private readonly string $id,
        private OrderStatus $status,
        private readonly Money $totalAmount
    ) {}

    public static function create(string $id, Money $amount): self
    {
        return new self($id, OrderStatus::Pending, $amount);
    }

    public function confirm(): void
    {
        if ($this->status !== OrderStatus::Pending) {
            throw new \DomainException('Only pending orders can be confirmed');
        }
        $this->status = OrderStatus::Confirmed;
    }

    public function status(): OrderStatus
    {
        return $this->status;
    }
}

Enforce invariants with value objects

Value objects like Money or EmailAddress validate themselves upon construction. This prevents primitive obsession and spreads validation logic across controllers. When your domain model is this strict, your Laravel testing with Pest in CI/CD becomes faster because you test business rules without booting the framework or hitting a database.

  • Never extend Eloquent Model in domain entities
  • Use enums for finite state sets (OrderStatus, PaymentType)
  • Throw DomainException for invariant violations, not HTTP exceptions
  • Keep entities serializable for event sourcing or caching needs

How do you wire use cases and dependency inversion in Laravel?

Use cases (or application services) orchestrate domain logic and infrastructure calls. They accept simple DTOs or primitives, never HTTP Request objects. Dependencies are injected via constructor using interfaces defined in the Application or Domain layer, with implementations bound in Laravel's service container.

ControllerInterface AdapterConfirmOrderUseCaseApplication LayerEloquentOrderRepoInfrastructureOrderRepositoryDomain Interfaceimplements
Dependency inversion: Controller depends on UseCase, UseCase depends on Repository interface, Infrastructure implements it

Define the use case contract

<?php
declare(strict_types=1);

namespace App\Application\Order;

interface ConfirmOrderUseCaseInterface
{
    public function execute(ConfirmOrderCommand $command): OrderDto;
}

final readonly class ConfirmOrderCommand
{
    public function __construct(
        public string $orderId,
        public string $confirmedByUserId
    ) {}
}

Implement and bind in ServiceProvider

The implementation retrieves the entity via repository, calls domain methods, persists changes, and dispatches events. Bind the interface to the concrete class in AppServiceProvider. This separation allows you to swap Eloquent for a DynamoDB adapter later without touching controllers or use cases. Teams adopting CI/CD pipelines with GitLab CI for Laravel benefit here because integration tests can use fake repositories while unit tests verify pure domain logic instantly.

How do you build infrastructure adapters without leaking framework details?

Infrastructure adapters translate between your domain and external tools. The most critical rule: domain entities never know about Eloquent. Map database models to domain entities inside the repository implementation, not in the entity itself.

Eloquent repository mapper pattern

<?php
declare(strict_types=1);

namespace App\Infrastructure\Persistence\Eloquent;

use App\Domain\Order\Order;
use App\Domain\Order\OrderRepositoryInterface;
use App\Models\OrderModel;

final class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function findById(string $id): ?Order
    {
        $model = OrderModel::find($id);
        return $model ? $this->toDomain($model) : null;
    }

    public function save(Order $order): void
    {
        $model = OrderModel::updateOrCreate(
            ['id' => $order->id()],
            [
                'status'       => $order->status()->value,
                'total_amount' => $order->totalAmount()->toCents(),
            ]
        );
    }

    private function toDomain(OrderModel $m): Order
    {
        return Order::reconstitute(
            $m->id,
            OrderStatus::from($m->status),
            Money::fromCents($m->total_amount)
        );
    }
}

This mapping overhead is the tax you pay for decoupling. It feels redundant for CRUD apps but prevents ORM changes from rippling through business logic. When optimizing performance, remember that Laravel performance optimization techniques like eager loading belong in the repository, not the domain. You can add query optimizations here without polluting use cases.

AspectStandard Laravel MVCClean Architecture Approach
Business Logic LocationControllers, Models, Form RequestsDomain Entities + Use Cases
Database CouplingEloquent embedded in modelsRepository abstraction with mappers
TestabilityRequires HTTP/DB bootstrappingPure unit tests for domain, fakes for infra
Framework UpgradesHigh risk, touches business codeLow risk, only adapter layer affected
Onboarding ComplexityLow initial, high long-termHigher initial, predictable long-term

When should you avoid clean architecture in Laravel projects?

Clean architecture adds indirection. For simple CRUD APIs, admin panels, or prototypes with stable requirements, standard MVC with service classes is often sufficient. The overhead of mappers, DTOs, and layered directories slows delivery when domain complexity is low.

Domain Complexity & Team SizeMaintenance ROIClean ArchitecturePays off after complexity thresholdStandard MVCFaster for simple CRUDInflection Point
Clean architecture ROI increases with domain complexity; standard MVC wins for low-complexity projects

Adopt clean architecture when you observe: multiple teams modifying the same models, frequent requirement changes in core business rules, need to support multiple delivery mechanisms (API, CLI, events), or compliance requirements demanding audit trails separate from storage. For Nepali fintech or e-commerce platforms handling payments and regulatory reporting, this separation often justifies the upfront cost. Conversely, internal tools with three developers and stable specs rarely need full DDD.

Practical next steps for adopting clean architecture in Laravel

Start incrementally. Extract one complex feature (order processing, payment reconciliation) into clean architecture layers while leaving simpler modules in MVC. Write characterization tests first to capture existing behavior before refactoring. Establish coding standards for entity purity and repository contracts early to prevent drift.

  1. Audit current pain points: identify controllers over 200 lines or models with business methods
  2. Create a src/Domain namespace outside app/ to enforce boundary discipline
  3. Write failing domain tests for invariants before extracting entities
  4. Build repository interfaces based on use case needs, not database schema
  5. Map Eloquent models to domain entities in infrastructure layer
  6. Bind interfaces in ServiceProvider and inject into use cases
  7. Add integration tests using test doubles for external services

Clean architecture in Laravel real example patterns are not about perfection but about managing change velocity. The goal is making future modifications cheaper than today's shortcuts. If your team struggles with flaky tests, slow onboarding, or fear of deploying core features, the layered approach addresses root causes rather than symptoms. Reach out via contact me if you need help assessing whether your Laravel codebase warrants this transition or want a targeted architecture review.

Frequently Asked Questions

A typical implementation separates domain entities, use cases, and infrastructure adapters. For instance, an e-commerce order service uses pure PHP classes for business logic while Laravel controllers and Eloquent repositories act solely as delivery and persistence adapters without coupling core rules to the framework.

Create src directory with Domain, Application, and Infrastructure layers. Place entities and value objects in Domain, use case interfaces and DTOs in Application, and Eloquent implementations plus API controllers in Infrastructure. Configure PSR-4 autoloading in composer.json to map these namespaces correctly for dependency injection.

Yes, typically.

Bind domain repository interfaces to Eloquent implementations in a service provider. Controllers and use cases depend only on abstractions. Laravel's container resolves concrete classes at runtime, allowing you to swap database drivers or external APIs without modifying business logic or violating dependency inversion principles.

No, avoid this.

Perform input validation in the presentation layer using Form Requests before reaching use cases. Execute business rule validation within domain entities or application services. This separation ensures technical constraints stay at the boundary while invariant checks remain encapsulated inside the core domain logic regardless of entry point.

Unit test domain entities and use cases with plain PHPUnit without database connections. Use integration tests for infrastructure adapters like repositories against test databases. Reserve feature tests for HTTP endpoints only. This layered approach keeps feedback loops fast and isolates failures to specific architectural boundaries during CI runs.

Identify bounded contexts first, then extract business logic from controllers into use cases. Create domain entities separate from Eloquent models. Introduce repository interfaces gradually, binding them to existing ORM code. Refactor incrementally per feature rather than rewriting everything at once to maintain deployment stability.

Not directly.

Define domain events as simple value objects within the core layer. Dispatch them from use cases after state changes. Listen and react in the infrastructure layer using Laravel's event system. This keeps business triggers framework-agnostic while allowing async processing, notifications, or projections through adapter implementations.

Developers often leak Eloquent into domain entities, create unnecessary abstraction layers for simple CRUD, or misplace validation logic. Another frequent error is treating every class as injectable when direct instantiation suffices. Focus on actual business complexity rather than dogmatic layering to avoid over-engineering maintainable applications.

Separate read and write models within the application layer. Write commands modify domain state through repositories while read queries bypass business logic entirely using optimized query builders. Both remain independent of controllers. This pattern scales complex Laravel systems where retrieval patterns differ significantly from transactional consistency requirements.

No official package exists, but community tools help. Packages like laravel-actions organize use cases, while spatie/laravel-data handles DTOs. These support clean structure without enforcing rigid frameworks. Evaluate whether they reduce boilerplate for your specific context instead of adopting them as mandatory architectural dependencies.

Maintain architecture decision records explaining why specific boundaries exist. Document module responsibilities, interface contracts, and data flow diagrams in markdown files alongside code. Update README sections describing layer interactions. This preserves institutional knowledge when team members rotate and prevents gradual erosion of architectural intent during future feature development cycles.

When maintenance overhead exceeds business value. If your team struggles with indirection, features ship slower, or the domain lacks genuine complexity, simplify. Revert to modular monolith or service-layer patterns. Architecture serves product delivery, not theoretical purity. Pragmatic adaptation beats dogmatic adherence in sustainable software engineering.