Architecture Layers

Layered Design & Responsibilities

Home | SoftLixx Creates
Architecture

Architecture Layers

Understanding the layered architecture that enforces separation of concerns and maintainability across the ERP system.

Layer Overview

AlKareem ERP follows a Domain-Driven Design (DDD) approach with a clean, layered architecture. Each layer has a single responsibility and communicates only with adjacent layers, ensuring loose coupling and high cohesion.

Layer Location Responsibility Depends On
Routes routes/{module}/ HTTP endpoints, middleware Livewire
Livewire app/Livewire/{Module}/ UI state, form validation Services
Services app/Services/{Module}/ Business logic, transactions Repositories, ApprovalEngine
Repositories app/Repositories/{Module}/ Data access, query building Models
Models app/Models/{Module}/ Schema, relationships, events Events
Events app/Events/{Module}/ Domain event definitions
Listeners app/Listeners/{Module}/ Side effects (GL posting, stock) Services

High-Level Data Flow

flowchart LR
    subgraph Presentation["🖥️ Presentation"]
        R[Route]
        L[Livewire Component]
    end
    
    subgraph Business["⚙️ Business Logic"]
        S[Service]
        AE[Approval Engine]
    end
    
    subgraph Data["💾 Data Access"]
        RP[Repository]
        M[Model]
    end
    
    subgraph Events["📡 Events"]
        E[Domain Event]
        LS[Listeners]
    end
    
    R --> L
    L -->|"validate & delegate"| S
    S -->|"check approval"| AE
    S -->|"data operations"| RP
    RP --> M
    M -->|"fireEvent()"| E
    E --> LS
    LS -->|"GL, Stock, etc."| S
    
    style Presentation fill:#0f172a,stroke:#38bdf8
    style Business fill:#0f172a,stroke:#a78bfa
    style Data fill:#0f172a,stroke:#34d399
    style Events fill:#0f172a,stroke:#fb923c
                        

Key Principle

Data flows downward through the layers (Route → Livewire → Service → Repository → Model), while events propagate outward to listeners. This ensures that side effects (like GL posting or stock updates) are decoupled from the main business logic.

Routes Layer

Responsibilities

  • Define HTTP endpoints
  • Apply authentication middleware
  • Apply Casbin authorization
  • Map to Livewire components

File Location

routes/{module}/*.php

Example: routes/inventory/transfers.php

Example: Transfer Routes

use App\Livewire\Inventory\Form\Transfers\{Index, Create, Edit};

Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/transfers', Index::class)
        ->middleware('casbin:TRANSFERS,VIEW');
    
    Route::get('/transfers/create', Create::class)
        ->middleware('casbin:TRANSFERS,CREATE');
    
    Route::get('/transfers/{id}/edit', Edit::class)
        ->middleware('casbin:TRANSFERS,UPDATE');
});

Livewire Layer

Should Do

  • UI state management
  • Form validation (Laravel rules)
  • User interaction handling
  • Delegate to Services
  • Display feedback (toasts)

Should NOT Do

  • Business logic
  • Direct database queries
  • Event dispatching
  • Complex calculations
  • Direct Model queries

Example: Create Transfer Component

class Create extends Component
{
    use Toast;
    
    public $issue_date;
    public $from_warehouse_id;
    public $to_warehouse_id;
    public $items = [];

    protected TransferService $transferService;

    public function boot(TransferService $transferService): void
    {
        $this->transferService = $transferService;
    }

    protected function rules(): array
    {
        return [
            'issue_date' => 'required|date',
            'from_warehouse_id' => 'required|exists:warehouses,id',
            'to_warehouse_id' => 'required|different:from_warehouse_id',
            'items' => 'required|array|min:1',
        ];
    }

    public function save(): void
    {
        $this->validate();
        
        // ✅ Delegate to Service - no business logic here!
        $this->transferService->create([
            'issue_date' => $this->issue_date,
            'from_warehouse_id' => $this->from_warehouse_id,
            'to_warehouse_id' => $this->to_warehouse_id,
            'items' => $this->items,
        ]);
        
        $this->success('Transfer created successfully.');
        $this->redirect('/transfers');
    }
}

Service Layer

The Heart of Business Logic

Services are where all business logic lives. They coordinate between repositories, handle transactions, integrate with the approval workflow, and control when domain events are fired.

Transaction Management

Wrap operations in DB::transaction()

Approval Integration

Check and create approval requests

Number Generation

Generate document numbers

Example: Transfer Service with Approval

class TransferService
{
    public function __construct(
        protected TransferRepository $transferRepository,
        protected ApprovalEngineService $approvalEngine
    ) {}

    public function create(array $data): Transfer
    {
        return DB::transaction(function () use ($data) {
            // Generate document number
            $data['transfer_number'] = $this->generateTransferNumber();
            
            // Create via repository
            $transfer = $this->transferRepository->create($data);

            // Check if approval is required
            if ($setup = $this->approvalEngine->checkSetupExists('CREATE', Transfer::class)) {
                // Approval required - create request, DON'T fire event
                $this->approvalEngine->createRequest(
                    $setup->id,
                    'TRANSFER_CREATE_APPROVAL',
                    $transfer->id,
                );
            } else {
                // No approval - auto-approve and fire event
                $transfer->status = 'APPROVED';
                $transfer->save();
                $transfer->fireCreateEvent();  // ← Only fire if auto-approved!
            }

            return $transfer;
        });
    }
}

Repository Layer

Repositories abstract data access and provide a clean API for querying and persisting entities. They handle eager loading, filtering, pagination, and basic CRUD operations.

Example: Transfer Repository

class TransferRepository
{
    public function paginate(array $filter = [], int $perPage = 15)
    {
        $query = Transfer::with(['fromWarehouse', 'toWarehouse', 'creator'])
            ->latest();

        if (!empty($filter['search'])) {
            $query->where('transfer_number', 'like', '%' . $filter['search'] . '%');
        }

        if (!empty($filter['status'])) {
            $query->where('status', $filter['status']);
        }

        return $query->paginate($perPage);
    }

    public function find($id): ?Transfer
    {
        return Transfer::with('items')->find($id);
    }

    public function create(array $data): Transfer
    {
        return DB::transaction(function () use ($data) {
            $transfer = Transfer::create($data);
            
            if (isset($data['items'])) {
                $transfer->items()->createMany($data['items']);
            }
            
            return $transfer;
        });
    }
}

Model Layer

Models define the database schema, relationships, and event firing methods. They implement the ProvidesApprovalData interface when approval workflow support is needed.

Key Responsibilities

  • Define $fillable & $casts
  • Relationship definitions
  • Event firing methods
  • Accessors and mutators

Event Methods

  • fireCreateEvent()
  • fireUpdateEvent()
  • fireDeleteEvent()

Example: Transfer Model

class Transfer extends Model implements ProvidesApprovalData
{
    public const STATUSES = [
        'PENDING', 'APPROVED', 'IN_TRANSIT', 'RECEIVED',
        'CANCELLED', 'REJECTED', 'AWAITING_UPDATE_APPROVAL',
    ];

    protected $fillable = [
        'transfer_number', 'from_warehouse_id', 'to_warehouse_id',
        'issue_date', 'status', 'remarks', 'created_by',
    ];

    // Relationships
    public function fromWarehouse(): BelongsTo
    {
        return $this->belongsTo(Warehouse::class, 'from_warehouse_id');
    }

    public function items(): HasMany
    {
        return $this->hasMany(TransferItem::class);
    }

    // Event firing methods
    public function fireCreateEvent(): void
    {
        $this->loadMissing('items');
        event(new TransferCreated($this));
    }

    // Approval interface
    public function getApprovalData(): array
    {
        $this->loadMissing('items');
        return array_merge($this->toArray(), [
            'items' => $this->items->toArray()
        ]);
    }
}