Use when creating a new Laravel project with DDD and Hexagonal Architecture, adding a new bounded context, or scaffolding a new module within an existing context.
Structure Laravel applications as bounded contexts with hexagonal layers. Each bounded context is a self-contained namespace division inside app/, wired via a dedicated ServiceProvider. One module per aggregate root. Dependencies flow inward only.
app/
├── {BoundedContext}/
│ ├── {BoundedContext}ServiceProvider.php
│ ├── Shared/ # Optional: cross-module concerns
│ └── {Module}/ # One module per aggregate root
│ ├── Application/
│ ├── Domain/
│ └── Infrastructure/
└── Shared/ # Shared Kernel (no aggregates)
├── SharedServiceProvider.php
├── Application/
├── Domain/
└── Infrastructure/
When creating a new module within a bounded context, scaffold the hexagonal structure. Items marked are required; others are added when the module needs them:
{Module}/
├── Application/
│ ├── Create{Entity}.php # Use case (always)
│ ├── Find{Entity}.php # Use case (always)
│ ├── Update{Entity}Information.php # Use case (when updatable)
│ ├── Delete{Entity}.php # Use case (when deletable)
│ └── EventHandlers/ # (when handling domain events)
├── Domain/
│ ├── {Entity}.php # Aggregate root — always
│ ├── {Entity}Factory.php # (when tests need factories)
│ ├── ValidationRules.php # (when entity has validation)
│ ├── Contracts/
│ │ └── {Entity}Repository.php # Interface — always
│ ├── Events/ # (when entity emits events)
│ │ ├── {Entity}Created.php
│ │ └── {Entity}Deleted.php
│ └── Exceptions/
│ ├── {Entity}Exception.php # Base exception — always
│ └── {Entity}NotFound.php # always
└── Infrastructure/
├── Http/ # (when entity has UI/API)
│ ├── Controllers/
│ │ ├── Create{Entity}Controller.php
│ │ ├── Show{Entity}Controller.php
│ │ ├── List{Entities}Controller.php
│ │ ├── Update{Entity}InformationController.php
│ │ ├── Delete{Entity}Controller.php
│ │ └── API/ # (when both web + API exist)
│ └── Requests/
│ ├── {Entity}Request.php
│ └── Filter{Entity}Request.php
└── Persistence/
├── Eloquent{Entity}Repository.php # always
├── Migrations/ # always
└── Seeders/ # (when seeding needed)
Each module directory maps to exactly one aggregate root entity. Child entities live inside the parent module's Domain/ directory — never in their own module.
# CORRECT: child entity inside parent module
Companies/Domain/Company.php # Aggregate root
Companies/Domain/CompanyContact.php # Child entity
# WRONG: child entity promoted to own module
Companies/Domain/Company.php
CompanyContacts/Domain/CompanyContact.php
| Layer | Contains | Depends On |
|---|---|---|
| Domain | Entities, VOs, enums, contracts, events, exceptions | Nothing (innermost) |
| Application | Use cases, event handlers, jobs, notifications | Domain only |
| Infrastructure | Controllers, repos, requests, persistence, mail | Application + Domain |
Dependencies flow inward only. Domain never imports from Application or Infrastructure.
app/Shared/)Cross-cutting contracts, traits, and base classes used by ALL bounded contexts. The Shared Kernel has no aggregates — it provides infrastructure only.
| Layer | Key Artifacts |
|---|---|
| Application | CanGenerateIdentifiers trait (UUID generation), VersionManager |
| Domain | HasDomainEvents trait, Event/EventBus contracts, HasProfilePhoto trait |
| Infrastructure | Base ServiceProvider, IlluminateEventBus, EloquentFactory, base controllers, middleware |
{BoundedContext}/Shared/)Optional directory for cross-module concerns within a single bounded context. Use when multiple modules in the same context share value objects, validators, or routes:
CompanyRegistry/
├── Shared/
│ ├── Domain/ # VOs, enums, validators shared across modules
│ └── Infrastructure/
│ └── Http/Routes/
│ ├── web.php # Context-level web routes
│ └── api.php # Context-level API routes
├── Companies/
├── Industries/
└── JobOffers/
Rule: If shared only within one context, use {BC}/Shared/. If shared across contexts, use app/Shared/.
App\{BoundedContext}\{Module}\{Layer}\...
App\IdentityAndAccess\Users\Domain\User
App\CompanyRegistry\Companies\Application\CreateCompany
App\CustomerRelationshipManagement\Contacts\Infrastructure\Persistence\EloquentContactRepository
Each bounded context uses a short prefix for its database tables. Choose a 2-4 letter lowercase abbreviation of the context name.
Example: CustomerRelationshipManagement → prefix crm_ → tables crm_contacts, crm_documents.
| Artifact | Convention | Example |
|---|---|---|
| Bounded Context dir | PascalCase multi-word | IdentityAndAccess/, CompanyRegistry/ |
| Module dir | PascalCase plural noun | Users/, Companies/, Visas/ |
| ServiceProvider | {BC}ServiceProvider | CompanyRegistryServiceProvider |
| Entity (root) | Singular noun | Company, User, Visa |
| Entity (child) | Parent-context noun | CompanyContact, Invitation |
| All files | PSR-4, one class per file | CreateCompany.php |
app/{BoundedContext}/{BoundedContext}ServiceProvider.php extending App\Shared\Infrastructure\ServiceProviderbootstrap/providers.php (after SharedServiceProvider){BC}/Shared/ for cross-module concerns and context-level routes| Mistake | Fix |
|---|---|
| Child entity in its own module | Keep inside parent module's Domain/ |
| Module with multiple aggregate roots | Split into separate modules |
| Domain importing from Infrastructure | Use contracts in Domain/Contracts/, implement in Infrastructure |
Cross-module code goes straight to app/Shared/ | Use context-level {BC}/Shared/ if only needed within one BC |
All migrations in database/migrations/ | Prefer Infrastructure/Persistence/Migrations/ per module |
Flat app/ with no bounded context grouping | Group by bounded context, not by layer |
Bounded contexts must not import directly from another context's Domain or Infrastructure. When Context A needs to reference an entity from Context B:
app/Shared/Domain/ that both contexts depend on.Never: Import App\IdentityAndAccess\Users\Domain\User from inside App\CustomerRelationshipManagement\. Instead, store user_id and resolve via a shared contract or event.