Blog
Using design patterns to build maintainable applications and reliable boundaries for AI-assisted development
Design patterns are most useful when they define clear responsibilities and enforceable boundaries. Combined carefully, Strategy, Factory, Adapter, Command, Specification, Repository and Decorator make Symfony and Laravel safer for AI-assisted changes.
Design patterns are a language, not a checklist
A pattern is useful when it names a real responsibility and makes an important part of a system easier to change. Adding Factory, Strategy or Repository simply to increase the class count has little value. Small PHP services can become less clear through needless layers; large applications can remain understandable through a few consistently protected boundaries.
That matters with AI-assisted code. A tool may generate valid syntax while placing a database query in a controller or an integration detail in business logic. Patterns and simple architectural rules give a team a language for reviewing that change.
A port is not its implementation
For order confirmation, the application can own a NotificationSender contract. SMTP, a provider API and a test implementation are adapters. Strategy makes sense where behaviour is selected at runtime from context; a Factory may select it. Where configuration fixes the implementation, ordinary dependency injection is often enough.
\\\text Use case ──► NotificationSender (port) ◄── SMTP / provider API / test adapter owned by application infrastructure boundary \\\
\\\`php interface NotificationSender { public function sendOrderConfirmation(OrderId $orderId): void; }
final readonly class SendOrderConfirmationService { public function __construct(private NotificationSender $sender) {}
public function send(OrderId $orderId): void { $this->sender->sendOrderConfirmation($orderId); } } \\\`
This is an illustrative excerpt: OrderId and the adapters are omitted. An interface implementation is not automatically a Strategy; that pattern needs interchangeable algorithms selected from runtime context.
Named operations and business rules
A Command can express intent and a Handler coordinate a use case. A Repository handles persistence, not an HTTP response; DTOs and Mappers earn their place where they clarify transformation or avoid unnecessary entity loading. A narrow read projection can be better than a mapper for every simple read.
A Specification is useful when a named business rule matters in more than one place. Ordinary form validation need not become a Specification. Ordered validators are not automatically Chain of Responsibility; that pattern needs its own passing-of-control contract.
Keep effects visible
A Decorator can add cache to a repository read without changing its contract. Recording a Domain Event does not ensure reliable asynchronous delivery or undo an external effect. Retry safety depends on idempotency, durable state and external side effects. Template Method fits a stable import lifecycle, but where only parsing or validation varies, Strategy or composition is often simpler; cleanup and failure handling still matter.
Reviewing AI-proposed changes
| Check | What it can establish | |---|---| | PHPStan | type problems and some contract mismatches | | Behavioural tests | whether an important use case still works | | Project rules | boundary violations, where separately implemented | | Human review | responsibility, side effects and trade-offs |
PHPStan and Pest do not enforce every architectural rule automatically. Project-specific rules, a dedicated tool or integration tests may be needed. Patterns do not make AI-generated code safe; they make a violated boundary easier to see.
Choose only what the operation needs
A document-generation operation does not need Command, Handler, Specification, Repository, Strategy, Factory, Adapter, Decorator and an event at once. Use components that solve the present problem. Good architecture is not a pattern catalogue; it is a set of decisions the next engineer or AI tool can understand, check and develop safely.
