ahmed-bhs/hexagonal-maker-bundle
layout: default
Everything is coupled anyway, so why bother?
Your code will always call other code. Repositories, services, databases—everything is connected. The question is not about eliminating coupling (impossible), but about controlling the direction of coupling.
In a traditional layered architecture, business logic (Services) depends directly on infrastructure (Database, Framework, Libraries). This creates a dangerous dependency chain with the following critical problems:
Core Principle: Make business logic independent by inverting who depends on whom.
Before diving into technical details, let's use a simple, everyday analogy:
Your Laptop and USB Devices
Think about your laptop and how it connects to external devices:
Key Insight: Your laptop doesn't care WHAT you plug in, as long as it respects the USB standard (interface).
What if USB didn't exist?
With USB (Ports & Adapters):
Mapping to Software:
| Real World | Software |
|---|---|
| 💻 Laptop | Domain (Business Logic) |
| 🔌 USB Port | Port Interface (Contract) |
| 🖱️ USB Mouse | Doctrine Adapter |
| ⌨️ USB Keyboard | MongoDB Adapter |
| 🖨️ USB Printer | Redis Adapter |
| 📱 USB Phone | InMemory Adapter (for tests) |
The Power:
UserRepositoryInterface is connected"Real Example in Code:
// 🌪️ Traditional: Domain depends on concrete MySQL
class OrderService {
public function __construct(
private DoctrineRepository $repo // Tightly coupled!
) {}
}
// Problem: Want MongoDB? Rewrite OrderService!
// 🎯 Hexagonal: Domain depends on interface (USB port)
class PlaceOrderHandler {
public function __construct(
private OrderRepositoryInterface $repo // Just a port!
) {}
}
// Solution: Want MongoDB? Create MongoOrderRepository implementing the interface!
Why This Matters:
Just like you can use your mouse on any laptop (Windows, Mac, Linux) because they all have USB ports, your business logic works with any database (MySQL, MongoDB, Redis) because they all implement your port interfaces.
You control the "USB standard" (interface), not the device manufacturers (infrastructure libraries).
Traditional (Dependencies flow DOWN):
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'16px'}}}%%
graph TD
A["🎮 Controllers<br/><small>UI Layer</small>"]
B["⚙️ Services<br/><small>Business Logic</small>"]
C["💾 Infrastructure<br/><small>Doctrine/Database</small>"]
A ==>|"🌪️ depends on"| B
B ==>|"🌪️ depends on"| C
style A fill:#E3F2FD,stroke:#1976D2,stroke-width:3px,color:#000
style B fill:#FFF9C4,stroke:#F57C00,stroke-width:3px,color:#000
style C fill:#FFCDD2,stroke:#C62828,stroke-width:3px,color:#000
classDef problemArrow stroke:#C62828,stroke-width:3px
Problem: Change database = rewrite business logic 🌪️
Hexagonal (Dependencies flow INWARD):
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'16px'}}}%%
graph BT
C["🔌 Infrastructure<br/><small>Doctrine/Database</small>"]
B["🔗 Ports<br/><small>Interfaces</small>"]
A["💎 Domain<br/><small>Business Logic - CORE</small>"]
C -.->|"🎯 implements"| B
B ==>|"🎯 defined by"| A
style A fill:#C8E6C9,stroke:#2E7D32,stroke-width:4px,color:#000,rx:10,ry:10
style B fill:#FFF9C4,stroke:#F9A825,stroke-width:3px,color:#000
style C fill:#F8BBD0,stroke:#C2185B,stroke-width:3px,color:#000
Solution: Change database = new adapter, business logic untouched 🎯
Traditional: Order entity has Doctrine annotations. Remove Doctrine? Domain breaks.
Hexagonal: Order is pure PHP with business rules. Infrastructure adapts to it. Remove Doctrine? Create a new adapter.
Key insight: Business logic doesn't know (and doesn't care) if data is stored in MySQL, MongoDB, Redis, or a text file. It defines WHAT it needs (interfaces/ports), and infrastructure provides HOW (adapters).
Back to the Laptop Analogy:
OrderRepositoryInterface"The Freedom This Gives You:
Layered Architecture Impact:
Hexagonal Architecture Impact:
Layered Architecture Reality:
Hexagonal Architecture Reality:
Impact: You can run 1000 hexagonal tests in the time layered runs 10 tests.
Laptop Analogy for Testing:
🔴 Traditional: Testing laptop functionality requires plugging in real mouse, keyboard, printer, etc.
🟢 Hexagonal: Testing laptop functionality with "mock USB devices"
This is exactly what in-memory repositories do - they're "mock USB devices" for your tests!
The Question: "Can we cancel a shipped order?"
Layered Architecture: Rule scattered across:
if ($status === 'shipped') return error;)Result: 4 different implementations, slight variations, which one is correct? Nobody knows.
Hexagonal Architecture: One method in Domain:
Order->cancel() throws exception if status is SHIPPED
Result: ONE source of truth. Crystal clear. Impossible to miss.
Layered Architecture Problem:
Hexagonal Architecture Solution:
Laptop Analogy for Multiple Interfaces:
🔴 Traditional: Want to use mouse with desktop, laptop, and tablet?
🟢 Hexagonal: One USB mouse works with ALL devices
Your business logic is the "mouse" - write it once, plug it into REST/GraphQL/CLI/gRPC!
| Benefit | Concrete Impact | Time Saved |
|---|---|---|
| Direction Control | Change database, framework, or any infrastructure without touching business logic | Weeks to Days |
| Single Source of Truth | Business rules in ONE place (Domain), not scattered across 10 files | 50% less bugs |
| Lightning Tests | 1000x faster (in-memory vs database I/O) | 10 min to 10 sec |
| Technology Freedom | Swap MySQL to MongoDB, Doctrine to Another ORM in days not months | 80% effort reduction |
| Reusability | Same business logic for REST, GraphQL, CLI, gRPC, message queue | Write once, use everywhere |
| Team Scalability | Juniors on adapters (infrastructure), Seniors on domain (business) | Clear separation of skill levels |
| Long-term Viability | Code survives framework updates, technology shifts, team changes | 10 years vs 10 months |
In traditional layered architecture, components are tightly coupled. When you add feature "X" after two years into the project, you must navigate code where business logic is mixed with database concerns and framework dependencies. Each modification risks breaking hidden dependencies.
The Problem: Time spent is no longer dedicated to coding the feature itself, but to:
Real Impact: A feature that should take 5 days now takes 15 days because:
The Solution: Because the domain is isolated, adding business feature "X" happens in a protected environment (the core). Technical complexity (ports and adapters) is pushed to the periphery.
Result: Similar features always cost approximately 5 days, because technical "friction" doesn't increase with business complexity growth.
Why This Works:
This is the most powerful technical argument for maintaining the 5-day velocity:
High-Fidelity, Fast Tests:
Immediate Feedback:
Example:
// 🎯 Hexagonal: Test in 10ms
$handler = new PlaceOrderHandler(new InMemoryOrderRepository());
$result = $handler->handle($command);
$this->assertTrue($result->isSuccess());
// 🌪️ Traditional: Test in 2-3 seconds
// - Boot Symfony kernel
// - Connect to database
// - Load fixtures
// - Execute test
// - Rollback transaction
Layered Architecture is Consumer Credit:
Hexagonal Architecture is Investment:
Year 1: Both architectures similar speed
Year 2:
Year 3:
Year 5:
The "5-Day Rule" in Practice:
Challenge: Change from MySQL (Doctrine ORM) to MongoDB (Document Database)
Laptop Analogy:
This is database migration in a nutshell. Let's see the real impact:
What must change:
[@ORM](https://github.com/ORM) annotations must be rewritten as documentsEstimated effort: 2-4 weeks of full-team work Risk level: HIGH - touching 60-80% of codebase Regression probability: Very high - every query must be rewritten and retested
What must change:
MongoUserRepository implements UserRepositoryInterfaceMongoOrderRepository implements OrderRepositoryInterfaceWhat stays the same:
Estimated effort: 1-2 days Risk level: LOW - only infrastructure adapters change Regression probability: Minimal - business logic untouched
The Math: Hexagonal saves you 10-20x the effort on technology changes.
| Criterion | Why It Matters |
|---|---|
| Long-term project (> 2 years) | Architecture ROI pays off over time as tech evolves |
| Growing team (> 3 devs) | Clear boundaries help multiple developers work in parallel |
| Complex business rules | Need single source of truth for domain logic |
| Multiple interfaces | REST + GraphQL + CLI + Events = reusable handlers |
| Tech might change | Framework updates, database migrations, cloud migrations |
| Testing is critical | Fast, reliable tests enable continuous deployment |
| Enterprise/production | Business continuity requires technology independence |
| Situation | Better Approach |
|---|---|
| Quick prototype (< 3 months) | Speed matters more than structure |
| Simple CRUD | Little to no business logic = overkill |
| Solo dev, tiny project | Overhead not justified |
| Stack 100% frozen | If you're SURE nothing will ever change (rarely true) |
What People Think Decoupling Means:
"My code doesn't depend on anything! Zero coupling!"
Reality: Impossible. Your code will always call other code. That's programming.
What Decoupling ACTUALLY Means:
"My business logic defines WHAT it needs (interfaces). Infrastructure provides HOW (implementations)."
Traditional Mindset:
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'15px'}}}%%
graph LR
A["🤔 What can we do with<br/>the tools we have?"]
B["🔒 Business logic limited<br/>by database capabilities"]
C["👀 Rules adapt to<br/>framework constraints"]
D["🌪️ Domain serves<br/>infrastructure"]
A ==> B ==> C ==> D
style A fill:#FFEBEE,stroke:#C62828,stroke-width:3px,color:#000
style B fill:#FFF3E0,stroke:#E65100,stroke-width:3px,color:#000
style C fill:#FFF3E0,stroke:#E65100,stroke-width:3px,color:#000
style D fill:#FFEBEE,stroke:#C62828,stroke-width:4px,color:#000
Hexagonal Mindset:
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'15px'}}}%%
graph LR
A["💡 What does the<br/>business need?"]
B["📋 Define domain<br/>rules first"]
C["🔧 Infrastructure adapts<br/>to serve those rules"]
D["🎯 Infrastructure<br/>serves domain"]
A ==> B ==> C ==> D
style A fill:#E8F5E9,stroke:#2E7D32,stroke-width:3px,color:#000
style B fill:#E8F5E9,stroke:#2E7D32,stroke-width:3px,color:#000
style C fill:#E1F5FE,stroke:#0277BD,stroke-width:3px,color:#000
style D fill:#E8F5E9,stroke:#2E7D32,stroke-width:4px,color:#000
Hexagonal architecture is not about:
Hexagonal architecture IS about:
The Question to Ask:
"If we need to change databases, frameworks, or add new interfaces next year, do I want to spend 2 weeks or 2 days?"
If your answer is "2 days," hexagonal architecture is your solution.
Remember: The coupling doesn't go away. You're still calling repositories and services. What changes is who is in charge—your business logic or your database.
Hexagonal Architecture isn't just a pattern—it's the natural embodiment of fundamental software engineering principles. Here's how it adheres to and enforces the most important design principles:
###...
How can I help you explore Laravel packages today?