4 min readCalm pace · scan the outline anytime

Anti-patterns

Common Modular Hexagonal DDD mistakes and the required fixes.

Anti-patterns

Use this page as a review checklist. Each row names the smell, why it breaks replaceability, and the fix.

Cross-module

Anti-patternWhy it is wrongFix
Use Case imports peer Domain / ApplicationCouples compile graph to peer internalsLocal ACL port + adapter, or Domain Event
Use Case injects peer *ModuleInterfaceSame coupling; fails HTTP-replace testConsumer-owned {Need}PortInterface
UI imports peer ORM models to decide policyBusiness rules leak into deliveryCall own Use Case; peer data via ACL
Shared folder holds business enums “for reuse”Shared becomes a distributed DomainACL / Events; Shared is technical only
Thin event with ID only; consumer re-fetches publisher DomainRe-introduces sync couplingRich happy-path payload (events)
Listener calls publisher Use Case / repositoryListener becomes an RPC clientMap from event payload → inbound Use Case
God *ModuleInterface dumping the whole moduleUnstable public API; hard to HTTP-replaceThin capability façades (contracts)
No eventId / idempotency on consumersDuplicate side effects under at-least-once deliveryEvent delivery
One SQL transaction writing two modules’ tablesHidden distributed transactionTransactions
ORM relations across module schemas for reportsRead-side couplingReads & reporting
Orchestrator imports three DomainsCollapses boundariesOrchestration — ACL/Events only

Layers and ports

Anti-patternWhy it is wrongFix
Controller / admin action calls repository directlySkips orchestration; logic spreadsController → first-level Use Case
Use Case uses ORM / query builderApplication learns persistence shapeDomain repository port
Entity extends ORM modelDomain inseparable from driverPlain Entity + Infrastructure record
Repository returns ORM modelsDomain/Application pollutedMap to Entity / Domain DTO
Domain Event uses framework traits / facadesDomain tied to hostPure event + Shared dispatcher port
Business rules inside composition rootWiring layer grows a second ApplicationMove to Use Case / Domain service

Use Cases and DTOs

Anti-patternWhy it is wrongFix
Literal Feature/ directoryPlaceholder mistaken for real pathReal capability name (Order/, …)
UI calls nested helper under UseCases/…/ProcessingBreaks entry-point boundaryCall first-level *UseCase only
Peer module calls this module’s Use CaseBreaks replaceabilityACL or Events
Application DTO used as Domain-wide shared type across modulesCross-module DTO couplingLocal DTOs; map at ACL boundary
Many public methods on a Use CaseUnclear entry; hard to testSingle __invoke on first-level Use Cases

Persistence and Shared

Anti-patternWhy it is wrongFix
Cross-module foreign keys for business joins via ORM relationsHidden coupling through the databaseACL reads / Events; ownership per module
Promote to Shared on first use “for the future”Premature kernelKeep in module until second consumer appears
SQL / vendor SDK inside DomainDomain not portablePort + Infrastructure adapter

Design test (final gate)

Before merge, answer again:

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

If not, you still have an anti-pattern above.

Return to the decision tree or walk the Ordering ↔ Warehouse cookbook.

Modular Hexagonal Domain-Driven Design
Core 1.0.0-draft