16 min readCalm pace · scan the outline anytime

Symfony one-shot apply (full architecture)

Self-contained Modular Hexagonal DDD + Symfony 8.1 apply playbook for AI agents.

Modular Hexagonal DDD — ONE-SHOT APPLY (symfony)

Self-contained. An AI agent should fetch this file only and execute it. Core version: 1.0.0-draftShort HTML entry: /symfony · This file: /apply/symfony.full.mdAI-neutral: assistant files are optional; architecture does not require any AI vendor. Host baseline researched: see Part C header (versions as of 2026-08).


0. Agent mandate

You are bootstrapping a symfony application repository (not the docs site).

  1. Treat this document as the complete working standard for this session.
  2. Execute Part D in order after reading Parts A–C.
  3. When writing module code, obey Part B (Core) and Part C (host).
  4. Do not create a git commit unless the human explicitly asks.
  5. Rename illustrative contexts Ordering, Warehouse, Directory, Identity to the human’s domains.
  6. If AI vendor is none, skip Part E.

Programmer prompt

Apply Modular Hexagonal DDD from <ORIGIN>/apply/symfony.full.md
AI assistant: <cursor|claude|gemini|copilot|generic|none>
Bounded contexts: <list>
Do not commit unless I ask.

Part A — Design test

If the peer module is deleted and replaced by HTTP, does this module’s Application and Domain still compile?


Part B — Complete Core architecture

B1. Purpose

Build applications as replaceable bounded-context modules using Hexagonal Architecture (ports & adapters) + DDD. This docs site is the sole canon; consuming repos keep thin pointers, not a forked essay.

B2. Module layout (MUST)

{Module}/
  Application/     # UseCases, Application DTO, Providers / composition root
  Domain/          # Entities, Ports, Events, Enums, Domain DTOs
  Infrastructure/  # Persistence adapters, ExternalServices (ACL), Config, listeners
  UI/              # Controllers, Routes, validation, admin UI, console
Shared/            # Promoted technical ports/adapters only (after promote rule)

Host roots vary (app/Modules, src/Modules, packages). Roles do not.

B3. Strictness ladder (MUST)

StrictnessAreaAllowedForbidden
HardestDomainOwn Domain types + Shared kernel portsOther modules; host facades/helpers; ORM; framework traits on Domain Events
StrictApplication Use Cases / App DTOsOwn Domain + Shared portsFacades, ORM, foreign Application/Domain, peer *ModuleInterface
RelaxedComposition rootContainer binds, routes, config mergeBusiness rules
OuterInfrastructure & UIFramework, ORM, HTTP, ACL adapters, adminLeaking ORM/foreign Domain into Domain/Use Cases

Dependency direction: Domain ← Application ← Infrastructure/UI. Ports in Domain; adapters in Infrastructure.

B4. Golden flow (MUST)

UI → Application DTO → first-level *UseCase::__invoke
  → Domain (Entity / Domain service)
  → Domain Port
  → Infrastructure adapter (ORM / ACL / HTTP)
  → back as Entity / result
  → UI presents

B5. Use Cases & DTOs (MUST)

  • First-level: Application/UseCases/{Capability}/SomethingUseCase with single public __invoke.
  • Nested helpers: no UseCase suffix; not called from UI.
  • Mirror {Capability}/ for DTOs and Ports.
  • ACL need ports: Domain/Ports/Acl/. Module façades: Domain/Ports/Module/.
  • Never create a literal Feature/ directory.
  • Application DTO = UI/CLI input. Domain DTO = intra-module only.
  • Other modules never call Use Cases — only ACL / Events.

B6. Cross-module bridges (MUST)

Application/Domain of A never import Application/Domain of B.

NeedBridge
Peer answer nowSync ACL: local {Need}PortInterface → Infra ACL adapter → thin peer {This}ModuleInterface
Peer reacts laterAsync Domain Event → consumer Infra translation listener → inbound Use Case
  • Mapping only in ACL adapter; façades thin + HTTP-mappable; Prefer ACL validate before local commit.
  • Events: pure Domain class; Shared EventDispatcherInterface; eventId + occurredAt + schemaVersion + rich happy-path payload; idempotent consumers; no Domain re-fetch on happy path; prefer outbox.
  • No cross-module write transaction. Reports: ACL reads or owned projections.

B7. Shared promote rule (MUST)

SituationWhere
Tech used by one moduleThat module’s Infrastructure
Same tech used by ≥2 modulesPromote to Shared
Cross-module business conceptNever Shared — ACL / Events

Typical Shared: DomainException, DatabaseTransactionInterface, EventDispatcherInterface, ClockInterface, ConfigReaderInterface, Ensure.

B8–B9. Persistence & UI (MUST)

  • Map persistence records ↔ Domain Entities in Infrastructure; never leak ORM into Use Cases/Domain.
  • UI: authorize → DTO → Use Case → present.

B10. Decision tree

Only this module? → Golden flow
Need peer now? → ACL
Peer later? → Event
Multi-step? → Orchestration (still ACL+Events)
Report across contexts? → Projection / ACL read
Tech in 2+ modules? → Shared
Business in 2+ modules? → ACL/Events — never Shared

B11. Anti-patterns

Peer imports in Use Cases; god façades; thin-ID events; Shared business enums; ORM in Domain; Entity extends ORM; literal Feature/; cross-module DB TX; ORM joins across modules for policy.

B12. Examples

Ordering, Warehouse, Directory, Identity only — rename to your domains.

B13. Quality assertions

Boundary tests must fail CI if: foreign Application/Domain imports; peer *ModuleInterface in Use Cases; ORM/facades in Domain/Use Cases; literal Feature/ folders. Also: behaviour tests, formatter, static analysis, event idempotency.


Part C — Symfony host mapping

TargetSymfony 8.1.x (current stable as of 2026-08; PHP ≥ 8.4)
Docshttps://symfony.com/doc/current/ · https://symfony.com/releases/8.1
Adapter HTML<ORIGIN>/v1/adapters/symfony

Use Flex app skeleton. Prefer one Symfony bundle (or src/ namespace) per bounded context with Core layers inside — not anemic src/Entity dump for all domains.

CoreSymfony 8.1 place
Module rootsrc/{Module}/ or src/{Module}Bundle/ (PSR-4)
Sharedsrc/Shared/
Composition rootconfig/services.yaml (+ per-module config/services/{module}.yaml) and/or bundle Extension / compiler passes — wiring only
PersistenceInfrastructure/Persistence/Doctrine/ — Entity (Doctrine) + Repository adapters mapping to Domain Entities
ACL adapterInfrastructure/ExternalServices/*AclAdapter.php as services
Translation listenerMessenger handler in consumer Infra → inbound Use Case (async transport)
UIUI/Http controllers / API Platform resource adapters calling Use Cases — not Domain

Doctrine (MUST quarantine)

  • Doctrine ORM entities/documents live in Infrastructure (or a clearly named Infra persistence model).
  • Domain Entities are plain PHP; repositories implement Domain ports and map to/from Doctrine models.
  • Never inject EntityManagerInterface into Use Cases or Domain.
  • Do not put Doctrine entities in messages — pass ids/primitives (Messenger docs warn about this).

DI

# config/services/ordering.yaml (example)
services:
  App\Modules\Ordering\Domain\Ports\Acl\WarehouseAvailabilityPortInterface:
    alias: App\Modules\Ordering\Infrastructure\ExternalServices\WarehouseAvailabilityAclAdapter

Autoconfigure is fine for Infra/UI; Domain stays free of Symfony attributes that pull the framework into the Domain model (prefer ports).

Messenger (Domain Events)

  • Publisher Use Case → Shared EventDispatcherInterface → adapter dispatches to Messenger.
  • Consumer: message handler in Infrastructure maps stamp/payload → Application DTO → inbound Use Case.
  • Use async transport (doctrine://, Redis, etc.); configure auto_setup=false in production for Doctrine transport.
  • Idempotency on eventId in consumer Infra.

Tooling

  • PHP-CS-Fixer / Symfony CS; PHPUnit or Pest; PHPStan level appropriate; optional Deptrac/Arkitect for layer rules.
  • bin/console for wiring — never for business rules inside Domain.

Part D — Apply procedure (Symfony)

D1 — Confirm host

Symfony Flex app with symfony/framework-bundle 8.1.* (or document older 7.4 LTS if constrained — still apply Core).

D2 — README pointer

Architecture: Modular Hexagonal Domain-Driven Design
One-shot: <ORIGIN>/apply/symfony.full.md
Host: Symfony 8.1 · PHP ≥ 8.4
Modules: src/{Module}/ · Shared: src/Shared/

D3–D4 — ROADMAP / optional PRD (product only)

D5 — Copy one-shot → .ai/modular-hexagonal-ddd/ARCHITECTURE.full.md

D6 — Composer PSR-4 for Modules + Shared

D7 — Optional first module under src/{Module}/{Application,Domain,Infrastructure,UI} + services yaml

D8 — Layer / arch tests (Deptrac or PHPStan custom) for Part B13

D9 — Part E then report — no commit unless asked


Part F — Sketch

Ordering ACL → Warehouse façade service; Messenger handles OrderFulfilled in Warehouse Infra handler.


Part G — Done

Pointers + .ai mirror + AI files + test plan; no unauthorized commit.


Part E — Assistant files (optional)

Skip if AI vendor is none.

Create shared folder:

.ai/modular-hexagonal-ddd/
  ARCHITECTURE.full.md   # copy of the host one-shot you fetched
  RULES.core.md
  SKILL.md
  CHECKLIST.md
  symfony-host.md         # host-only notes from Part C

E0 — RULES.core.md

# Modular Hexagonal DDD — Core rules
Canon: <ORIGIN>/v1/core (1.0.0-draft)
One-shot: <ORIGIN>/apply/symfony.full.md
Local: .ai/modular-hexagonal-ddd/ARCHITECTURE.full.md

1. Domain hardest: no host framework, ORM, other modules.
2. Use Cases: own Domain + Shared ports only.
3. Cross-module: ACL or Domain Events only.
4. Thin façades; rich events (eventId, schemaVersion); idempotent consumers.
5. No cross-module write TX; ACL validate-before-commit.
6. Shared = technical ≥2 modules only.
7. *UseCase + __invoke; no literal Feature/ folders.
8. Design test: peer→HTTP, Application/Domain still compile.

E1 — SKILL.md

---
name: modular-hexagonal-ddd
description: Enforce Modular Hexagonal DDD. Modules, ACL, Events, Shared, Use Cases.
---
Follow .ai/modular-hexagonal-ddd/ARCHITECTURE.full.md

E2 — CHECKLIST.md

- [ ] First-level *UseCase entry
- [ ] App DTO → __invoke
- [ ] I/O via Domain ports
- [ ] No ORM/facades in Application/Domain
- [ ] Sync peer → local ACL port → thin façade
- [ ] Async → rich event + translation listener → inbound UC
- [ ] Idempotent eventId
- [ ] No cross-module DB TX
- [ ] No foreign Application/Domain imports
- [ ] Shared only technical ≥2
- [ ] No Feature/ directories
- [ ] Replaceability test = yes

E3 — Cursor

  • .cursor/rules/modular-hexagonal-ddd.mdc ← E0 + path to ARCHITECTURE.full.md
  • .cursor/rules/symfony-host.mdc ← Part C summary
  • .cursor/skills/modular-hexagonal-ddd/SKILL.md ← E1

E4 — Claude

  • CLAUDE.md ← pointer to ARCHITECTURE.full.md + E0
  • Shared .ai/modular-hexagonal-ddd/*

E5 — Gemini

  • GEMINI.md or .gemini/instructions.md ← pointer + E0
  • Shared .ai/*

E6 — GitHub Copilot

  • .github/copilot-instructions.md ← E0 + short host section

E7 — Generic

  • AGENTS.md pointing at .ai/modular-hexagonal-ddd/ARCHITECTURE.full.md

E8 — Multiple vendors

One shared .ai/modular-hexagonal-ddd/; vendor entry files only point to it.

Modular Hexagonal Domain-Driven Design
Core 1.0.0-draft