Port Interface Design Principles

Table of Contents

  1. What is a Port?
  2. Naming Conventions
  3. Interface Segregation Principle (ISP)
  4. Method Design Guidelines
  5. Common Port Patterns
  6. Anti-Patterns to Avoid
  7. 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) or Domain/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, not persist)
  • 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 (object and string are 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 PlaceOrderUseCaseInterface requires 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/ or Domain/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

Next: Primary vs Secondary Adapters β†’