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()orhandle()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
ModelBasewhich 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();
}
}