Domain Architecture

Learn the folder layout and architectural layers of Obelaw Domains.

Domain Architecture

Each domain in Obelaw follows a strict Domain-Driven Design (DDD) layout. This ensures that reads, writes, and business logic stay separated and encapsulated.

Directory Structure

A complete domain namespace is structured as follows:

src/
├── Actions/       # Write operations (Mutations)
├── Queries/       # Read pipelines (Retrievals)
├── Data/          # Immutable Data Transfer Objects (DTOs)
├── Models/        # Persistence-only Eloquent Models
├── Managers/      # Entry point / Orchestration Services
├── Events/        # Domain lifecycle events
├── Exceptions/    # Domain-specific exceptions
└── Traits/        # Reusable model behaviors

1. Actions (Write Mutations)

Actions are responsible for mutations. They represent a single unit of work (e.g., CreateOrder, UpdateStock).

  • Every Action must have a single public execute() or handle() method.
  • Actions accept a validated DTO and return a result (or throw an exception).
  • They must not be called directly from controllers; they are invoked via the Domain Manager.
namespace Domains\Inventory\Actions;

use Domains\Inventory\Data\StockData;
use Domains\Inventory\Models\Stock;

class AdjustStock
{
    public function execute(StockData $data): Stock
    {
        $stock = Stock::where('product_id', $data->productId)->firstOrFail();
        $stock->increment('quantity', $data->quantity);
        return $stock;
    }
}

2. Queries (Read Pipelines)

Queries are responsible for fetching data.

  • They implement a fluid query builder pattern.
  • Intermediate methods (like filters) must return $this.
  • Terminal methods (like get(), first(), paginate()) materialize and return the results as DTOs or collections.
namespace Domains\Inventory\Queries;

class StockQuery
{
    protected $query;

    public function __construct()
    {
        $this->query = Stock::query();
    }

    public function inWarehouse(int $warehouseId): self
    {
        $this->query->where('warehouse_id', $warehouseId);
        return $this;
    }

    public function availableOnly(): self
    {
        $this->query->where('quantity', '>', 0);
        return $this;
    }

    public function get(): Collection
    {
        return $this->query->get();
    }
}

3. Data (DTOs)

Data Transfer Objects (DTOs) enforce type-safety and consistency at domain boundaries.

  • They are immutable.
  • They validate incoming payloads before they touch your business logic.
namespace Domains\Inventory\Data;

use Spatie\LaravelData\Data;

class StockData extends Data
{
    public function __construct(
        public int $productId,
        public int $quantity,
        public ?int $warehouseId = null,
    ) {}
}

4. Models (Persistence-Only)

Eloquent models in Obelaw are persistence definitions only.

  • They must not contain business logic or validations.
  • They extend ModelBase which automatically handles database routing, table prefixes, and connections.
namespace Domains\Inventory\Models;

use Obelaw\Framework\Models\ModelBase;

class Stock extends ModelBase
{
    protected ?string $module = 'inventory';
    protected $fillable = ['product_id', 'quantity', 'warehouse_id'];
}

5. Managers (Entry Points)

The Manager acts as the gateway to the domain.

  • It exposes public methods to outer layers.
  • It instantiates and runs the internal Actions and Queries.
namespace Domains\Inventory\Manager;

use Domains\Inventory\Actions\AdjustStock;
use Domains\Inventory\Queries\StockQuery;
use Domains\Inventory\Data\StockData;

class InventoryManager
{
    public function adjustStock(StockData $data)
    {
        return app(AdjustStock::class)->execute($data);
    }

    public function stockQuery(): StockQuery
    {
        return new StockQuery();
    }
}