Port Interface Design Principles
Table of Contents
- What is a Port?
- Naming Conventions
- Interface Segregation Principle (ISP)
- Method Design Guidelines
- Common Port Patterns
- Anti-Patterns to Avoid
- Real-World Examples
What is a Port?
A Port is an interface defined in the Domain layer that declares what the domain needs from the outside world.
Domain defines: "I need to save users" β UserRepositoryInterface (Port)
Infrastructure provides: "Here's how" β DoctrineUserRepository (Adapter)
Key Characteristics
- Defined in Domain - Lives in
Domain/Port/In/(driving) orDomain/Port/Out/(driven) - Input Ports (In) - Define what the application offers, implemented by Application layer
- Output Ports (Out) - Define what the application needs, implemented by Infrastructure layer
- Expresses Business Intent - Uses domain language, not technical language
- No Implementation Details - No mention of Doctrine, MySQL, HTTP, etc.
Port Types
Domain/Port/
βββ In/ # Input/Driving Ports (Primary)
β βββ CreateUserUseCaseInterface.php # Implemented by Application layer
βββ Out/ # Output/Driven Ports (Secondary)
βββ UserRepositoryInterface.php # Implemented by Infrastructure layer
Port In vs Port Out - Complete Explanation
Port In (Driving / Primary Port)
Definition: An interface that defines what the application offers to the outside world.
Question to ask: βWho initiates the action?β β If the outside world initiates, itβs a Port In.
Analogy: The front door of your house. Visitors knock on it to request something from you.
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Controller β ββββββ> β Port/In β ββββββ> β UseCase β
β (UI) β calls β (interface) β impl by β (App) β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
OUTSIDE DOMAIN APPLICATION
(calls us) (contract) (does the work)
Characteristics:
- Defined in
Domain/Port/In/ - Implemented by Application layer (Use Cases)
- Called by UI layer (Controllers, CLI, API)
- Represents a business capability the application exposes
Examples:
// "The application CAN register users"
interface RegisterUserUseCaseInterface {
public function execute(RegisterUserCommand $command): UserId;
}
// "The application CAN place orders"
interface PlaceOrderUseCaseInterface {
public function execute(PlaceOrderCommand $command): OrderId;
}
// "The application CAN cancel subscriptions"
interface CancelSubscriptionUseCaseInterface {
public function execute(CancelSubscriptionCommand $command): void;
}
Port Out (Driven / Secondary Port)
Definition: An interface that defines what the application needs from the outside world.
Question to ask: βWho initiates the action?β β If the application initiates, itβs a Port Out.
Analogy: The back door of your house. You go through it to get something you need (groceries, mail, etc.).
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β UseCase β ββββββ> β Port/Out β ββββββ> β Adapter β
β (App) β uses β (interface) β impl by β (Infra) β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
APPLICATION DOMAIN INFRASTRUCTURE
(needs sth) (contract) (provides it)
Characteristics:
- Defined in
Domain/Port/In/ - Implemented by Infrastructure layer (Adapters)
- Used by Application layer (Use Cases)
- Represents a dependency the application requires
Examples:
// "The application NEEDS to persist users"
interface UserRepositoryInterface {
public function save(User $user): void;
public function findById(UserId $id): ?User;
}
// "The application NEEDS to calculate taxes"
interface TaxCalculatorInterface {
public function calculate(Money $amount, Country $country): Money;
}
// "The application NEEDS to check stock"
interface InventoryCheckerInterface {
public function isAvailable(ProductId $id, int $quantity): bool;
}
Visual Summary
WHO CALLS? WHO IMPLEMENTS?
βββββββββββ
β UI β (Controller, CLI)
ββββββ¬βββββ
β calls
βΌ
βββββββββββββββ βββββββββββββββ
β Port/In β βββββββββββββββββ Application β
β (interface) β implemented β (UseCase) β
βββββββββββββββ by ββββββββ¬βββββββ
β uses
βΌ
βββββββββββββββ
βββββββββββββββ implemented β Port/Out β
β Infra β βββββββββββββββ>β (interface) β
β (Adapter) β by βββββββββββββββ
βββββββββββββββ
Architecture Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β π DOMAIN (Hexagon) β
β β
β Port/In/ (What the application CAN DO) β
β βββ RegisterUserUseCaseInterface β
β βββ PlaceOrderUseCaseInterface β
β βββ CancelSubscriptionUseCaseInterface β
β βββ ApplyDiscountUseCaseInterface β
β β
β Port/Out/ (What the application NEEDS) β
β βββ UserRepositoryInterface # Persistence β
β βββ PricingServiceInterface # Price calc β
β βββ TaxCalculatorInterface # VAT calc β
β βββ InventoryCheckerInterface # Stock check β
β βββ FraudDetectionInterface # Anti-fraud β
β βββ LoyaltyPointsServiceInterface # Loyalty points β
β βββ ShippingCostCalculatorInterface # Shipping fees β
β βββ InvoiceGeneratorInterface # Invoicing β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β² β²
β β
ββββββββββββ΄βββββββββ βββββββββ΄ββββββββββββββββ
β APPLICATION β β INFRASTRUCTURE β
β (implements β β (implements β
β Port/In) β β Port/Out) β
β β β β
β PlaceOrderUseCase β β StripePricingService β
β RegisterUserCase β β TaxJarCalculator β
β β β WarehouseInventory β
β Orchestrates: β β SiftFraudDetection β
β - check stock β β ColissimoShipping β
β - calc price β β DoctrineRepositories β
β - detect fraud β β β
βββββββββββββββββββββ βββββββββββββββββββββββββ
Dependency flow:
- Application β depends on β Domain (uses ports)
- Infrastructure β depends on β Domain (implements Port/Out)
- UI β depends on β Application (calls use cases via Port/In)
Naming Conventions
Repository Ports
β GOOD:
interface UserRepositoryInterface // Clear: manages User entities
interface OrderRepositoryInterface // Clear: manages Order entities
interface ProductRepositoryInterface // Clear: manages Product entities
β BAD:
interface UserDAO // Technical term (Data Access Object)
interface UserPersistence // Vague
interface IUserRepository // Hungarian notation (avoid "I" prefix)
interface UserRepositoryPort // Redundant suffix
Service Ports
β GOOD:
interface EmailSenderInterface // Clear capability
interface PaymentProcessorInterface // Clear responsibility
interface NotificationServiceInterface // Clear purpose
β BAD:
interface EmailService // Too vague
interface IEmailSender // Hungarian notation
interface SMTPEmailSender // Implementation detail leaked!
Query Ports (CQRS)
β GOOD:
interface UserQueryInterface // Clear: read operations for Users
interface OrderQueryInterface // Clear: read operations for Orders
interface ProductCatalogQueryInterface // Clear: specific read concern
β BAD:
interface UserReader // Unclear
interface GetUserQuery // Not a capability, but an action
Interface Segregation Principle (ISP)
βClients should not be forced to depend on methods they do not use.β
The Problem: Fat Interfaces
β BAD: God Interface
interface UserRepositoryInterface
{
// Read methods
public function findById(UserId $id): ?User;
public function findByEmail(string $email): ?User;
public function findAll(): array;
public function findActiveUsers(): array;
public function findUsersByRole(string $role): array;
public function searchUsers(string $query): array;
// Write methods
public function save(User $user): void;
public function delete(User $user): void;
// Statistics methods
public function countUsers(): int;
public function countActiveUsers(): int;
// Admin methods
public function purgeInactiveUsers(): void;
public function exportUsersToCSV(): string;
// Notification methods
public function findUsersToNotify(): array;
}
Problems:
- Handler that only saves users depends on 15 methods it doesnβt need
- Hard to test (must mock 15 methods)
- Hard to implement (adapter must implement everything)
- Violates Single Responsibility Principle
The Solution: Segregated Interfaces
β GOOD: Segregated by Responsibility
// Write operations
interface UserRepositoryInterface
{
public function save(User $user): void;
public function delete(User $user): void;
public function existsByEmail(string $email): bool;
}
// Read operations (CQRS pattern)
interface UserQueryInterface
{
public function findById(UserId $id): ?User;
public function findByEmail(string $email): ?User;
public function findActiveUsers(): array;
}
// Admin operations
interface UserAdminInterface
{
public function purgeInactiveUsers(): void;
public function countUsers(): int;
}
// Notification operations
interface UserNotificationQueryInterface
{
public function findUsersToNotify(): array;
}
Benefits:
- Handlers depend only on what they need
- Easy to test (mock only relevant methods)
- Easy to implement (adapter implements one responsibility at a time)
- Clear separation of concerns
When to Split vs Keep Together
β Keep together when methods are always used together:
// GOOD: These methods logically belong together
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function delete(Order $order): void;
}
β Split when methods serve different use cases:
// BAD: findPendingOrders is specific to a background job
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function findPendingOrders(): array; // β Different concern!
}
// GOOD: Separate query interface
interface OrderQueryInterface
{
public function findPendingOrders(): array;
}
Method Design Guidelines
1. Use Domain Language, Not Technical Language
β GOOD: Domain Language
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function findPendingOrders(): array; // Business concept
}
β BAD: Technical Language
interface OrderRepositoryInterface
{
public function persist(Order $order): void; // Technical (SQL term)
public function selectById(OrderId $id): ?Order; // Technical (SQL term)
public function queryByStatusPending(): array; // Technical implementation detail
}
2. Return Domain Objects, Not Primitives
β GOOD: Domain Objects
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function findActiveUsers(): array; // array<User>
}
β BAD: Primitives
interface UserRepositoryInterface
{
public function findById(string $id): ?array; // array is not type-safe
public function findActiveUsers(): array; // array<what?>
}
Use PHPDoc for clarity:
interface UserRepositoryInterface
{
/**
* @return array<User>
*/
public function findActiveUsers(): array;
}
3. Accept Domain Types, Not Primitives
β GOOD: Value Objects
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function existsByEmail(Email $email): bool;
}
β BAD: Primitives
interface UserRepositoryInterface
{
public function findById(string $id): ?User;
public function existsByEmail(string $email): bool; // Loses domain validation
}
Why? Value objects ensure validation happens at the boundary, not in the adapter.
4. Design for Readability
Method names should read like natural language.
β GOOD: Readable
if ($this->users->existsByEmail($email)) {
throw new EmailAlreadyExistsException();
}
$orders = $this->orders->findPendingOrders();
β BAD: Unclear
if ($this->users->checkEmail($email)) { // Check what about email?
throw new EmailAlreadyExistsException();
}
$orders = $this->orders->getPending(); // Get pending what?
5. Avoid Leaking Implementation Details
β GOOD: Implementation-Agnostic
interface NotificationServiceInterface
{
public function send(Notification $notification): void;
}
β BAD: Leaks Implementation
interface NotificationServiceInterface
{
public function sendViaSmtp(Notification $notification): void; // β SMTP is implementation detail
public function sendViaSendGrid(Notification $notification): void; // β SendGrid is implementation detail
}
Why? Port should describe βwhatβ, not βhowβ. Implementation can change without changing the port.
6. Design for Testability
Ports should be easy to mock/stub.
β GOOD: Simple, Testable
interface EmailSenderInterface
{
public function send(Email $email): void;
}
// Test with in-memory fake
class InMemoryEmailSender implements EmailSenderInterface
{
private array $sentEmails = [];
public function send(Email $email): void
{
$this->sentEmails[] = $email;
}
public function getSentEmails(): array
{
return $this->sentEmails;
}
}
β BAD: Hard to Test
interface EmailSenderInterface
{
public function send(
Email $email,
EmailConfiguration $config,
TransportOptions $transport,
RetryPolicy $retry
): SendResult;
}
// Test requires complex setup with many dependencies
Common Port Patterns
Pattern 1: Repository Port (Persistence)
Purpose: Manage aggregate root lifecycle (CRUD).
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function delete(Order $order): void;
}
Key Points:
- One repository per aggregate root
- Methods use domain language (
save, notpersist) - Return domain entities, not arrays
Pattern 2: Query Port (CQRS Read Side)
Purpose: Optimized read operations, may return DTOs instead of entities.
interface ProductCatalogQueryInterface
{
/**
* @return array<ProductListDTO>
*/
public function findAvailableProducts(int $limit, int $offset): array;
public function findProductById(ProductId $id): ?ProductDetailDTO;
public function searchProducts(string $query): array;
}
Key Points:
- Separate from write operations (repository)
- Can return DTOs optimized for display
- May bypass domain entities for performance
Pattern 3: External Service Port
Purpose: Communicate with external systems (email, payment, etc.).
interface PaymentProcessorInterface
{
public function charge(PaymentRequest $request): PaymentResult;
public function refund(RefundRequest $request): RefundResult;
}
Key Points:
- Express business capability, not technical protocol
- Accept/return domain objects
- Hide implementation details (Stripe, PayPal, etc.)
Pattern 4: Event Dispatcher Port
Purpose: Publish domain events.
interface EventDispatcherInterface
{
public function dispatch(DomainEvent $event): void;
}
Key Points:
- Generic interface for all events
- Domain events are first-class citizens
- Infrastructure handles routing
Pattern 5: Specification Port (Query Builder)
Purpose: Build complex queries dynamically.
interface UserSpecificationInterface
{
public function matching(Specification $spec): array;
}
// Usage
$activeAdmins = $this->users->matching(
new AndSpecification(
new IsActiveSpecification(),
new HasRoleSpecification(Role::ADMIN)
)
);
Key Points:
- Allows complex filtering without polluting repository
- Composable specifications
- Advanced pattern, use sparingly
Anti-Patterns to Avoid
Anti-Pattern 1: Generic Repository
β AVOID:
interface GenericRepositoryInterface
{
public function save(object $entity): void;
public function findById(string $id): ?object;
public function findAll(): array;
}
Problems:
- Type-unsafe (
objectandstringare too generic) - Loses domain specificity
- No type hinting benefits
β BETTER:
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
}
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
Anti-Pattern 2: Repositories with Business Logic
β AVOID:
interface OrderRepositoryInterface
{
public function save(Order $order): void;
// β Business logic leaked into repository!
public function cancelOrder(OrderId $id): void;
public function shipOrder(OrderId $id, Address $address): void;
}
Problem: Repository should manage persistence, not execute business logic.
β BETTER:
// Repository: persistence only
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
// Business logic in handlers
class CancelOrderHandler
{
public function __invoke(CancelOrderCommand $command): void
{
$order = $this->orders->findById($command->orderId);
$order->cancel(); // Business logic in entity
$this->orders->save($order);
}
}
Anti-Pattern 3: Query Methods Returning Scalar Arrays
β AVOID:
interface UserRepositoryInterface
{
/**
* @return array<array{id: string, email: string, name: string}>
*/
public function findAllUsers(): array;
}
Problem: Array shapes are error-prone and not type-safe.
β BETTER:
interface UserQueryInterface
{
/**
* @return array<UserListDTO>
*/
public function findAllUsers(): array;
}
final readonly class UserListDTO
{
public function __construct(
public string $id,
public string $email,
public string $name,
) {}
}
Anti-Pattern 4: Ports Depending on Infrastructure
β AVOID:
use Doctrine\ORM\EntityManagerInterface;
interface UserRepositoryInterface
{
public function getEntityManager(): EntityManagerInterface; // β Leaks infrastructure!
}
Problem: Domain now depends on Doctrine.
β BETTER:
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
// No mention of Doctrine, EntityManager, or any framework
}
Real-World Examples
Port/Out Examples (Business Needs)
| Port Out | Business Need | Possible Implementations |
|---|---|---|
PricingServiceInterface |
Calculate final price (promos, B2B, etc.) | StripePricing, CustomPricingEngine |
TaxCalculatorInterface |
Calculate VAT by country/product | TaxJarAPI, GovernmentTaxAPI |
InventoryCheckerInterface |
Check stock availability | WarehouseAPI, ERPConnector |
FraudDetectionInterface |
Detect suspicious orders | SiftScience, Signifyd |
LoyaltyPointsServiceInterface |
Manage loyalty points | ZendeskLoyalty, InternalSystem |
ShippingCostCalculatorInterface |
Calculate shipping fees | Colissimo, UPS, FedEx |
CreditCheckInterface |
Check B2B client solvency | CreditSafe, Dun&Bradstreet |
InvoiceGeneratorInterface |
Generate compliant invoices | StripeInvoicing, QuickBooks |
Port/In Examples (Business Capabilities)
| Port In | Business Use Case | Called By |
|---|---|---|
PlaceOrderUseCaseInterface |
Place an order | Web, Mobile, Partner API |
ApplyDiscountUseCaseInterface |
Apply promo code | Checkout, Customer service |
RequestRefundUseCaseInterface |
Request refund | Customer, Support |
UpgradeSubscriptionUseCaseInterface |
Change plan | Customer portal, Sales |
ReportSuspiciousActivityInterface |
Report fraud | Auto system, Admin |
Example 1: E-Commerce Order System
// Domain/Port/In/ - What the application CAN DO
interface PlaceOrderUseCaseInterface
{
public function execute(PlaceOrderCommand $command): OrderId;
}
interface CancelOrderUseCaseInterface
{
public function execute(CancelOrderCommand $command): void;
}
// Domain/Port/Out/ - What the application NEEDS (Business services)
interface PricingServiceInterface
{
public function calculateFinalPrice(Cart $cart, ?PromoCode $code): Money;
}
interface TaxCalculatorInterface
{
public function calculateTax(Money $amount, Country $country, ProductType $type): Money;
}
interface InventoryCheckerInterface
{
public function isAvailable(ProductId $productId, int $quantity): bool;
public function reserveStock(ProductId $productId, int $quantity): void;
}
interface FraudDetectionInterface
{
public function assessRisk(Order $order, Customer $customer): RiskScore;
}
interface ShippingCostCalculatorInterface
{
public function calculate(Address $address, Weight $weight): Money;
}
// Domain/Port/Out/ - Persistence
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
Example 2: User Authentication System
// User persistence
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
public function findByEmail(Email $email): ?User;
public function existsByEmail(Email $email): bool;
}
// Password hashing (external service)
interface PasswordHasherInterface
{
public function hash(string $plaintext): string;
public function verify(string $plaintext, string $hash): bool;
}
// Email notifications
interface EmailSenderInterface
{
public function send(Email $email): void;
}
// Token generation
interface TokenGeneratorInterface
{
public function generate(): string;
}
Example 3: Blog System
// Article persistence
interface ArticleRepositoryInterface
{
public function save(Article $article): void;
public function findById(ArticleId $id): ?Article;
public function delete(Article $article): void;
}
// Article queries (optimized for performance)
interface ArticleQueryInterface
{
/**
* @return array<ArticleSummaryDTO>
*/
public function findPublishedArticles(int $limit, int $offset): array;
public function findArticleBySlug(string $slug): ?ArticleDetailDTO;
/**
* @return array<ArticleSummaryDTO>
*/
public function findArticlesByAuthor(AuthorId $authorId): array;
public function countPublishedArticles(): int;
}
// Search functionality
interface ArticleSearchInterface
{
/**
* @return array<ArticleSearchResultDTO>
*/
public function search(string $query): array;
}
// Image storage
interface ImageStorageInterface
{
public function store(Image $image): string; // Returns URL
public function delete(string $url): void;
}
Quiz: Port In or Port Out?
Test your understanding! For each interface, decide if itβs a Port In or Port Out.
| Interface | Your Answer | Correct Answer |
|---|---|---|
RegisterUserUseCaseInterface |
? | In - What the app offers |
UserRepositoryInterface |
? | Out - App needs persistence |
PlaceOrderUseCaseInterface |
? | In - What the app offers |
PaymentGatewayInterface |
? | Out - App needs payment service |
CancelSubscriptionUseCaseInterface |
? | In - What the app offers |
EmailSenderInterface |
? | Out - App needs email service |
TaxCalculatorInterface |
? | Out - App needs tax calculation |
ApplyDiscountUseCaseInterface |
? | In - What the app offers |
InventoryCheckerInterface |
? | Out - App needs stock info |
FraudDetectionInterface |
? | Out - App needs fraud check |
Quick Rules
Port In = Ends with UseCaseInterface β Implemented by Application
Port Out = Ends with RepositoryInterface, ServiceInterface, Interface β Implemented by Infrastructure
Why Separate In and Out?
1. Clarity of Responsibility
Without separation: Who implements UserServiceInterface? Application? Infrastructure? Unclear.
With separation:
Port/In/ β "Here's what I CAN DO for you" β Application implements
Port/Out/ β "Here's what I NEED from you" β Infrastructure implements
| Port Type | Meaning | Implemented By |
|---|---|---|
Port/In/RegisterUserUseCaseInterface |
βI can register usersβ | Application\RegisterUserUseCase |
Port/Out/UserRepositoryInterface |
βI need user storageβ | Infrastructure\DoctrineUserRepository |
Port/Out/EmailSenderInterface |
βI need email sendingβ | Infrastructure\SymfonyMailerAdapter |
2. Dependency Inversion Principle (DIP)
The separation enforces dependencies pointing inward (toward Domain):
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β UI (Controller) β
β β β
β β depends on β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β DOMAIN β β
β β β β
β β Port/In/PlaceOrderUseCaseInterface ββββββ β β
β β β β β
β β Port/Out/OrderRepositoryInterface ββββββΌβββββ β β
β β Port/Out/PaymentGatewayInterface ββββββΌβββββΌββββ β β
β β β β β β β
β ββββββββββββββββββββββββββββββββββββββββββββββΌβββββΌββββΌββββ β
β β β β β
β APPLICATION ββββββββββββββββββββββββββββββββββ β β β
β (implements Port/In, uses Port/Out) β β β
β β β β
β INFRASTRUCTURE ββββββββββββββββββββββββββββββββββββ΄ββββ β
β (implements Port/Out) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
All arrows point TOWARD Domain = Domain has ZERO external dependencies
Why this matters:
- Domain never changes when you change database (Doctrine β MongoDB)
- Domain never changes when you change payment provider (Stripe β PayPal)
- Domain is pure business logic, testable without any framework
3. Testability
Testing Port/In (Application layer):
// Mock the Port/Out dependencies
$mockRepository = $this->createMock(OrderRepositoryInterface::class);
$mockPayment = $this->createMock(PaymentGatewayInterface::class);
// Test the use case in isolation
$useCase = new PlaceOrderUseCase($mockRepository, $mockPayment);
$orderId = $useCase->execute($command);
$this->assertNotNull($orderId);
Testing Port/Out (Infrastructure layer):
// Test the real adapter against a test database
$repository = new DoctrineOrderRepository($entityManager);
$repository->save($order);
$found = $repository->findById($order->getId());
$this->assertEquals($order, $found);
Test Pyramid with Ports:
/\
/ \ E2E Tests (few, slow)
/ββββ\ Test full flow: UI β Port/In β App β Port/Out β Infra
/ \
/ββββββββ\ Integration Tests (moderate)
/ \ Test Port/Out implementations with real DB
/ββββββββββββ\
/ \ Unit Tests (many, fast)
/ \ Test Application via Port/In with mocked Port/Out
/ββββββββββββββββββ\
4. Team Organization
| Team | Responsibility | Works With |
|---|---|---|
| Product/Domain experts | Define Port/In interfaces | βWhat should the app do?β |
| Application developers | Implement Port/In (Use Cases) | Business logic orchestration |
| Infrastructure developers | Implement Port/Out (Adapters) | Technical integrations |
Parallel development:
Week 1:
βββ Team A: Defines Port/In/PlaceOrderUseCaseInterface
βββ Team B: Defines Port/Out/InventoryCheckerInterface
βββ Team C: Defines Port/Out/PaymentGatewayInterface
Week 2 (parallel work):
βββ Team A: Implements PlaceOrderUseCase (mocks Port/Out)
βββ Team B: Implements WarehouseInventoryAdapter
βββ Team C: Implements StripePaymentAdapter
Week 3:
βββ Integration: Connect real adapters, everything works!
5. Swappable Infrastructure
Scenario: Switch from Stripe to PayPal
Without Port/Out separation:
// Business logic coupled to Stripe π±
class PlaceOrderUseCase {
public function __construct(private StripeClient $stripe) {}
public function execute($command) {
$this->stripe->charges->create([...]); // Stripe-specific!
}
}
// To switch to PayPal: rewrite ALL business logic π
With Port/Out separation:
// Business logic uses abstraction π
class PlaceOrderUseCase {
public function __construct(private PaymentGatewayInterface $payment) {}
public function execute($command) {
$this->payment->charge($amount); // Abstract!
}
}
// config/services.yaml - Just change one line!
services:
App\Domain\Port\Out\PaymentGatewayInterface:
class: App\Infrastructure\Payment\PayPalAdapter # Was StripeAdapter
Real-world flexibility:
# Development
App\Domain\Port\Out\PaymentGatewayInterface:
class: App\Infrastructure\Payment\FakePaymentAdapter
# Testing
App\Domain\Port\Out\PaymentGatewayInterface:
class: App\Infrastructure\Payment\InMemoryPaymentAdapter
# Production (France)
App\Domain\Port\Out\PaymentGatewayInterface:
class: App\Infrastructure\Payment\StripeAdapter
# Production (Germany) - different provider required by law
App\Domain\Port\Out\PaymentGatewayInterface:
class: App\Infrastructure\Payment\KlarnaAdapter
6. Business Logic Protection
Port/In protects your business capabilities:
- The interface is a contract with the outside world
- Changing
PlaceOrderUseCaseInterfacerequires updating all callers - Forces you to think about backward compatibility
Port/Out protects from infrastructure changes:
- Database migration? Only change the adapter
- New payment provider? Only add a new adapter
- Email service down? Swap to backup adapter
Business Logic (stable) βββ Protected by Ports βββ Infrastructure (volatile)
PlaceOrderUseCase DoctrineOrderRepository
β β
β uses β can be replaced by
βΌ βΌ
OrderRepositoryInterface ββββββββββββββββββ MongoOrderRepository
(never changes) (new implementation)
Decision Checklist
When designing a port, ask yourself:
- Does the interface name clearly express its purpose?
- Are methods named using domain language, not technical terms?
- Does it accept/return domain objects (entities, value objects, DTOs)?
- Is it segregated (ISP)βhandlers depend only on what they need?
- Does it avoid leaking implementation details?
- Can it be easily mocked/stubbed for testing?
- Would a business expert understand the method names?
- Is it defined in the Domain layer (
Domain/Port/In/orDomain/Port/Out/)? - Does it have zero dependencies on infrastructure?
Summary
| Principle | Guideline |
|---|---|
| Naming | Use domain language, avoid technical terms |
| Segregation | Split interfaces by responsibility (ISP) |
| Types | Accept/return domain objects, not primitives |
| Clarity | Methods should read like natural language |
| Abstraction | Hide implementation details completely |
| Testability | Easy to mock with in-memory fakes |
| Location | Always in Domain/Port/In/ or Domain/Port/Out/, never in Infrastructure |