15 min read

🔷 Hexagonal Architecture (Ports & Adapters)

📚 İçindekiler

  1. Hexagonal Architecture Nedir?
  2. Neden Hexagonal Architecture?
  3. Temel Kavramlar
  4. Klasör Yapısı
  5. Detaylı Açıklamalar
  6. Gerçek Proje Örneği
  7. Best Practices
  8. Avantajlar ve Dezavantajlar

Hexagonal Architecture Nedir?

Hexagonal Architecture (Altıgen Mimari), Alistair Cockburn tarafından 2005 yılında önerilmiş bir yazılım mimari desenidir. Ports and Adapters Architecture olarak da bilinir.

🎯 Ana Fikir

Uygulamanın çekirdeğini (core) dış dünyadan izole etmek. Uygulama, dış sistemlere (veritabanı, API'ler, UI) bağımlı olmadan çalışabilmeli.

🔶 Neden Altıgen?

Altıgen şekli semboliktir. Aslında kaç kenarı olduğu önemli değil. Önemli olan:

  • Merkez: Uygulama çekirdeği (domain logic)
  • Kenarlar: Dış dünya ile bağlantı noktaları (ports)
  • Dışarı: Adaptörler (adapters)
        [Web UI Adapter]
              ↓
        [HTTP Port]
              ↓
    ┌─────────────────────┐
    │                     │
    │   APPLICATION       │
    │      CORE           │
    │   (Domain Logic)    │
    │                     │
    └─────────────────────┘
              ↓
        [Repository Port]
              ↓
        [PostgreSQL Adapter]

Neden Hexagonal Architecture?

❌ Geleneksel Mimari Problemleri

Layered Architecture'da:

UI → Business Logic → Data Access → Database

Problemler:

  • Business logic veritabanına bağımlı
  • Test etmek için database gerekli
  • Framework değişikliği zor
  • Dış sistemler değişince core etkilenir

✅ Hexagonal Architecture Çözümü

         [Adapters]
            ↕️
         [Ports]
            ↕️
    [Application Core]
            ↕️
         [Ports]
            ↕️
         [Adapters]

Avantajlar:

  • ✅ Core bağımsız
  • ✅ Test edilebilir
  • ✅ Framework agnostic
  • ✅ Adapter değiştirilebilir

Temel Kavramlar

1️⃣ Application Core (Uygulama Çekirdeği)

Sorumluluk: İş mantığı, domain kuralları

Özellikleri:

  • Hiçbir dış bağımlılık yok
  • Framework agnostic
  • Database agnostic
  • UI agnostic

İçerir:

  • Domain entities
  • Business rules
  • Use cases
  • Domain services

2️⃣ Ports (Portlar)

Port, bir interface'dir. Dış dünya ile iletişim kurallarını tanımlar.

🔵 Primary Ports (Driving Ports) - Gelen Portlar

Tanım: Uygulamanın sunduğu servisler

Kim kullanır: Dış aktörler (UI, API clients, test'ler)

Örnek:

// Primary Port - Uygulama bu servisi sunar
type ProductService interface {
    CreateProduct(name string, price float64) (*Product, error)
    GetProduct(id int64) (*Product, error)
    UpdatePrice(id int64, newPrice float64) error
    DeleteProduct(id int64) error
}

Yön: Dış Dünya → Port → Application Core


🔴 Secondary Ports (Driven Ports) - Giden Portlar

Tanım: Uygulamanın ihtiyaç duyduğu servisler

Kim implement eder: Dış sistemler (database, external APIs, email services)

Örnek:

// Secondary Port - Uygulama buna ihtiyaç duyar
type ProductRepository interface {
    Save(product *Product) error
    FindById(id int64) (*Product, error)
    FindAll() ([]*Product, error)
    Delete(id int64) error
}

type EmailService interface {
    SendNotification(to string, message string) error
}

type PaymentGateway interface {
    ProcessPayment(amount float64, card string) error
}

Yön: Application Core → Port → Dış Sistem


3️⃣ Adapters (Adaptörler)

Adapter, port'ları implement eden concrete implementasyonlardır.

🔵 Primary Adapters (Driving Adapters)

Tanım: Uygulamayı kullanan adaptörler

Örnekler:

  • REST API controller
  • gRPC server
  • CLI (Command Line Interface)
  • GraphQL resolver
  • Web UI controller
  • Test adapter

Örnek:

// HTTP Adapter - REST API
type HTTPProductHandler struct {
    productService ports.ProductService  // Primary port kullanır
}

func (h *HTTPProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
    // HTTP request'i parse et
    var req CreateProductRequest
    json.NewDecoder(r.Body).Decode(&req)
    
    // Core service'i çağır (primary port üzerinden)
    product, err := h.productService.CreateProduct(req.Name, req.Price)
    
    // HTTP response'u oluştur
    json.NewEncoder(w).Encode(product)
}

🔴 Secondary Adapters (Driven Adapters)

Tanım: Uygulama tarafından kullanılan adaptörler

Örnekler:

  • PostgreSQL repository
  • MongoDB repository
  • Redis cache
  • SMTP email service
  • AWS S3 storage
  • Stripe payment gateway
  • Mock/Fake implementations (test için)

Örnek:

// PostgreSQL Adapter
type PostgresProductRepository struct {
    db *sql.DB
}

// Secondary port'u implement eder
func (r *PostgresProductRepository) Save(product *Product) error {
    query := "INSERT INTO products (name, price) VALUES ($1, $2)"
    _, err := r.db.Exec(query, product.Name, product.Price)
    return err
}

func (r *PostgresProductRepository) FindById(id int64) (*Product, error) {
    query := "SELECT id, name, price FROM products WHERE id = $1"
    row := r.db.QueryRow(query, id)
    
    var product Product
    err := row.Scan(&product.ID, &product.Name, &product.Price)
    return &product, err
}

Klasör Yapısı

📁 Temel Yapı

project/
├── core/                    # Application Core
│   ├── domain/             # Domain entities, value objects
│   ├── ports/              # Port interface'leri
│   └── services/           # Core services (use cases)
│
├── adapters/               # Adaptörler
│   ├── primary/           # Driving adapters (inbound)
│   │   ├── http/          # REST API
│   │   ├── grpc/          # gRPC server
│   │   ├── cli/           # Command line
│   │   └── graphql/       # GraphQL
│   │
│   └── secondary/         # Driven adapters (outbound)
│       ├── postgresql/    # PostgreSQL repository
│       ├── mongodb/       # MongoDB repository
│       ├── redis/         # Redis cache
│       ├── smtp/          # Email service
│       └── stripe/        # Payment gateway
│
├── config/                 # Configuration
├── cmd/                    # Application entry points
└── tests/                  # Tests

Detaylı Açıklamalar

📂 core/domain/

Sorumluluk: Pure business entities ve domain logic

Özellikleri:

  • Hiçbir framework'e bağımlı değil
  • Hiçbir infrastructure'a bağımlı değil
  • Sadece Go standard library kullanabilir

İçerik:

core/domain/
├── product.go              # Entity
├── order.go                # Entity
├── customer.go             # Entity
├── money.go                # Value object
├── email.go                # Value object
├── errors.go               # Domain errors
└── events.go               # Domain events

Örnek - product.go:

package domain

import (
    "errors"
    "time"
)

// Domain Entity
type Product struct {
    ID          int64
    Name        string
    Price       Money      // Value object
    Category    string
    Stock       int
    IsActive    bool
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

// Business logic in entity
func (p *Product) ChangePrice(newPrice Money) error {
    if newPrice.Amount <= 0 {
        return errors.New("price must be positive")
    }
    
    p.Price = newPrice
    p.UpdatedAt = time.Now()
    return nil
}

func (p *Product) IsInStock() bool {
    return p.Stock > 0 && p.IsActive
}

func (p *Product) DecreaseStock(quantity int) error {
    if quantity > p.Stock {
        return errors.New("insufficient stock")
    }
    
    p.Stock -= quantity
    return nil
}

// Value Object
type Money struct {
    Amount   float64
    Currency string
}

func NewMoney(amount float64, currency string) Money {
    return Money{
        Amount:   amount,
        Currency: currency,
    }
}

func (m Money) Add(other Money) (Money, error) {
    if m.Currency != other.Currency {
        return Money{}, errors.New("currency mismatch")
    }
    
    return Money{
        Amount:   m.Amount + other.Amount,
        Currency: m.Currency,
    }, nil
}

📂 core/ports/

Sorumluluk: Interface tanımları (sözleşmeler)

İçerik:

core/ports/
├── primary/
│   ├── product_service.go
│   ├── order_service.go
│   └── customer_service.go
│
└── secondary/
    ├── product_repository.go
    ├── order_repository.go
    ├── email_service.go
    ├── payment_gateway.go
    └── cache_service.go

Örnek - primary/product_service.go:

package primary

import "myapp/core/domain"

// Primary Port - Application'ın sunduğu servis
type ProductService interface {
    CreateProduct(name string, price domain.Money, category string) (*domain.Product, error)
    GetProduct(id int64) (*domain.Product, error)
    UpdatePrice(id int64, newPrice domain.Money) error
    DeleteProduct(id int64) error
    ListProducts(page, size int) ([]*domain.Product, error)
    SearchProducts(query string) ([]*domain.Product, error)
}

Örnek - secondary/product_repository.go:

package secondary

import "myapp/core/domain"

// Secondary Port - Application'ın ihtiyaç duyduğu servis
type ProductRepository interface {
    Save(product *domain.Product) error
    FindById(id int64) (*domain.Product, error)
    FindAll(page, size int) ([]*domain.Product, error)
    Update(product *domain.Product) error
    Delete(id int64) error
    Search(query string) ([]*domain.Product, error)
}

Örnek - secondary/email_service.go:

package secondary

// Secondary Port
type EmailService interface {
    SendProductCreatedNotification(productName string, to []string) error
    SendLowStockAlert(productName string, stock int) error
}

Örnek - secondary/payment_gateway.go:

package secondary

import "myapp/core/domain"

// Secondary Port
type PaymentGateway interface {
    ProcessPayment(amount domain.Money, cardToken string) (transactionId string, err error)
    RefundPayment(transactionId string) error
    GetPaymentStatus(transactionId string) (string, error)
}

📂 core/services/

Sorumluluk: Use case implementasyonları (application logic)

İçerik:

core/services/
├── product_service.go
├── order_service.go
└── customer_service.go

Örnek - product_service.go:

package services

import (
    "myapp/core/domain"
    "myapp/core/ports/primary"
    "myapp/core/ports/secondary"
    "time"
)

// ProductService implements primary.ProductService
type ProductService struct {
    productRepo  secondary.ProductRepository  // Secondary port dependency
    emailService secondary.EmailService       // Secondary port dependency
    cache        secondary.CacheService        // Secondary port dependency
}

// Constructor - dependency injection
func NewProductService(
    repo secondary.ProductRepository,
    email secondary.EmailService,
    cache secondary.CacheService,
) primary.ProductService {
    return &ProductService{
        productRepo:  repo,
        emailService: email,
        cache:        cache,
    }
}

// Primary port implementation
func (s *ProductService) CreateProduct(name string, price domain.Money, category string) (*domain.Product, error) {
    // Business validation
    if name == "" {
        return nil, errors.New("product name is required")
    }
    
    if price.Amount <= 0 {
        return nil, errors.New("price must be positive")
    }
    
    // Create domain entity
    product := &domain.Product{
        Name:      name,
        Price:     price,
        Category:  category,
        Stock:     0,
        IsActive:  true,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }
    
    // Use secondary port to save
    err := s.productRepo.Save(product)
    if err != nil {
        return nil, err
    }
    
    // Use another secondary port for notification
    s.emailService.SendProductCreatedNotification(product.Name, []string{"[email protected]"})
    
    // Invalidate cache
    s.cache.Delete("products:all")
    
    return product, nil
}

func (s *ProductService) GetProduct(id int64) (*domain.Product, error) {
    // Try cache first
    cacheKey := fmt.Sprintf("products:%d", id)
    if cached := s.cache.Get(cacheKey); cached != nil {
        return cached.(*domain.Product), nil
    }
    
    // Use secondary port to fetch
    product, err := s.productRepo.FindById(id)
    if err != nil {
        return nil, err
    }
    
    // Cache it
    s.cache.Set(cacheKey, product, 5*time.Minute)
    
    return product, nil
}

func (s *ProductService) UpdatePrice(id int64, newPrice domain.Money) error {
    // Fetch product
    product, err := s.GetProduct(id)
    if err != nil {
        return err
    }
    
    // Use domain logic
    err = product.ChangePrice(newPrice)
    if err != nil {
        return err
    }
    
    // Persist
    err = s.productRepo.Update(product)
    if err != nil {
        return err
    }
    
    // Invalidate cache
    s.cache.Delete(fmt.Sprintf("products:%d", id))
    
    return nil
}

📂 adapters/primary/http/

Sorumluluk: HTTP REST API adapter

İçerik:

adapters/primary/http/
├── handlers/
│   ├── product_handler.go
│   ├── order_handler.go
│   └── health_handler.go
├── middleware/
│   ├── auth.go
│   ├── logging.go
│   └── cors.go
├── dto/
│   ├── product_dto.go
│   └── error_dto.go
├── router.go
└── server.go

Örnek - product_handler.go:

package handlers

import (
    "encoding/json"
    "net/http"
    "strconv"
    
    "myapp/core/domain"
    "myapp/core/ports/primary"
    "github.com/gorilla/mux"
)

// HTTP Adapter
type ProductHandler struct {
    productService primary.ProductService  // Depends on PRIMARY PORT
}

func NewProductHandler(service primary.ProductService) *ProductHandler {
    return &ProductHandler{
        productService: service,
    }
}

// DTO for HTTP request
type CreateProductRequest struct {
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Currency string  `json:"currency"`
    Category string  `json:"category"`
}

// DTO for HTTP response
type ProductResponse struct {
    ID       int64   `json:"id"`
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Currency string  `json:"currency"`
    Category string  `json:"category"`
}

// HTTP Handler
func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
    // 1. Parse HTTP request
    var req CreateProductRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }
    
    // 2. Convert DTO to domain
    price := domain.NewMoney(req.Price, req.Currency)
    
    // 3. Call core service (through PRIMARY PORT)
    product, err := h.productService.CreateProduct(req.Name, price, req.Category)
    if err != nil {
        http.Error(w, err.Error(), http.StatusUnprocessableEntity)
        return
    }
    
    // 4. Convert domain to DTO
    response := ProductResponse{
        ID:       product.ID,
        Name:     product.Name,
        Price:    product.Price.Amount,
        Currency: product.Price.Currency,
        Category: product.Category,
    }
    
    // 5. Send HTTP response
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(response)
}

func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
    // Parse path parameter
    vars := mux.Vars(r)
    id, _ := strconv.ParseInt(vars["id"], 10, 64)
    
    // Call service
    product, err := h.productService.GetProduct(id)
    if err != nil {
        http.Error(w, "Product not found", http.StatusNotFound)
        return
    }
    
    // Convert and respond
    response := ProductResponse{
        ID:       product.ID,
        Name:     product.Name,
        Price:    product.Price.Amount,
        Currency: product.Price.Currency,
        Category: product.Category,
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

Örnek - router.go:

package http

import (
    "myapp/adapters/primary/http/handlers"
    "myapp/core/ports/primary"
    "github.com/gorilla/mux"
)

func SetupRoutes(productService primary.ProductService) *mux.Router {
    router := mux.NewRouter()
    
    // Initialize handlers
    productHandler := handlers.NewProductHandler(productService)
    
    // Product routes
    router.HandleFunc("/api/products", productHandler.CreateProduct).Methods("POST")
    router.HandleFunc("/api/products/{id}", productHandler.GetProduct).Methods("GET")
    router.HandleFunc("/api/products/{id}", productHandler.UpdatePrice).Methods("PATCH")
    router.HandleFunc("/api/products/{id}", productHandler.DeleteProduct).Methods("DELETE")
    router.HandleFunc("/api/products", productHandler.ListProducts).Methods("GET")
    
    return router
}

📂 adapters/primary/grpc/

Sorumluluk: gRPC adapter

İçerik:

adapters/primary/grpc/
├── proto/
│   └── product.proto
├── generated/
│   ├── product.pb.go
│   └── product_grpc.pb.go
└── product_service.go

Örnek - product.proto:

syntax = "proto3";

package product;
option go_package = "myapp/adapters/primary/grpc/generated";

service ProductService {
    rpc CreateProduct(CreateProductRequest) returns (ProductResponse);
    rpc GetProduct(GetProductRequest) returns (ProductResponse);
}

message CreateProductRequest {
    string name = 1;
    double price = 2;
    string currency = 3;
    string category = 4;
}

message GetProductRequest {
    int64 id = 1;
}

message ProductResponse {
    int64 id = 1;
    string name = 2;
    double price = 3;
    string currency = 4;
    string category = 5;
}

Örnek - product_service.go:

package grpc

import (
    "context"
    
    "myapp/core/domain"
    "myapp/core/ports/primary"
    pb "myapp/adapters/primary/grpc/generated"
)

// gRPC Adapter
type ProductGRPCService struct {
    pb.UnimplementedProductServiceServer
    productService primary.ProductService  // Depends on PRIMARY PORT
}

func NewProductGRPCService(service primary.ProductService) *ProductGRPCService {
    return &ProductGRPCService{
        productService: service,
    }
}

func (s *ProductGRPCService) CreateProduct(ctx context.Context, req *pb.CreateProductRequest) (*pb.ProductResponse, error) {
    // Convert gRPC request to domain
    price := domain.NewMoney(req.Price, req.Currency)
    
    // Call core service
    product, err := s.productService.CreateProduct(req.Name, price, req.Category)
    if err != nil {
        return nil, err
    }
    
    // Convert domain to gRPC response
    return &pb.ProductResponse{
        Id:       product.ID,
        Name:     product.Name,
        Price:    product.Price.Amount,
        Currency: product.Price.Currency,
        Category: product.Category,
    }, nil
}

func (s *ProductGRPCService) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.ProductResponse, error) {
    product, err := s.productService.GetProduct(req.Id)
    if err != nil {
        return nil, err
    }
    
    return &pb.ProductResponse{
        Id:       product.ID,
        Name:     product.Name,
        Price:    product.Price.Amount,
        Currency: product.Price.Currency,
        Category: product.Category,
    }, nil
}

📂 adapters/secondary/postgresql/

Sorumluluk: PostgreSQL repository adapter

İçerik:

adapters/secondary/postgresql/
├── product_repository.go
├── order_repository.go
├── migrations/
│   ├── 001_create_products.sql
│   └── 002_create_orders.sql
└── connection.go

Örnek - product_repository.go:

package postgresql

import (
    "database/sql"
    "fmt"
    
    "myapp/core/domain"
    "myapp/core/ports/secondary"
)

// PostgreSQL Adapter - implements SECONDARY PORT
type PostgresProductRepository struct {
    db *sql.DB
}

func NewPostgresProductRepository(db *sql.DB) secondary.ProductRepository {
    return &PostgresProductRepository{db: db}
}

// Implement secondary port
func (r *PostgresProductRepository) Save(product *domain.Product) error {
    query := `
        INSERT INTO products (name, price, currency, category, stock, is_active, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        RETURNING id
    `
    
    err := r.db.QueryRow(
        query,
        product.Name,
        product.Price.Amount,
        product.Price.Currency,
        product.Category,
        product.Stock,
        product.IsActive,
        product.CreatedAt,
        product.UpdatedAt,
    ).Scan(&product.ID)
    
    return err
}

func (r *PostgresProductRepository) FindById(id int64) (*domain.Product, error) {
    query := `
        SELECT id, name, price, currency, category, stock, is_active, created_at, updated_at
        FROM products
        WHERE id = $1
    `
    
    var product domain.Product
    var priceAmount float64
    var priceCurrency string
    
    err := r.db.QueryRow(query, id).Scan(
        &product.ID,
        &product.Name,
        &priceAmount,
        &priceCurrency,
        &product.Category,
        &product.Stock,
        &product.IsActive,
        &product.CreatedAt,
        &product.UpdatedAt,
    )
    
    if err != nil {
        return nil, err
    }
    
    product.Price = domain.NewMoney(priceAmount, priceCurrency)
    return &product, nil
}

func (r *PostgresProductRepository) FindAll(page, size int) ([]*domain.Product, error) {
    offset := (page - 1) * size
    query := `
        SELECT id, name, price, currency, category, stock, is_active, created_at, updated_at
        FROM products
        ORDER BY created_at DESC
        LIMIT $1 OFFSET $2
    `
    
    rows, err := r.db.Query(query, size, offset)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var products []*domain.Product
    for rows.Next() {
        var product domain.Product
        var priceAmount float64
        var priceCurrency string
        
        err := rows.Scan(
            &product.ID,
            &product.Name,
            &priceAmount,
            &priceCurrency,
            &product.Category,
            &product.Stock,
            &product.IsActive,
            &product.CreatedAt,
            &product.UpdatedAt,
        )
        
        if err != nil {
            return nil, err
        }
        
        product.Price = domain.NewMoney(priceAmount, priceCurrency)
        products = append(products, &product)
    }
    
    return products, nil
}

func (r *PostgresProductRepository) Update(product *domain.Product) error {
    query := `
        UPDATE products
        SET name = $1, price = $2, currency = $3, category = $4, 
            stock = $5, is_active = $6, updated_at = $7
        WHERE id = $8
    `
    
    _, err := r.db.Exec(
        query,
        product.Name,
        product.Price.Amount,
        product.Price.Currency,
        product.Category,
        product.Stock,
        product.IsActive,
        product.UpdatedAt,
        product.ID,
    )
    
    return err
}

func (r *PostgresProductRepository) Delete(id int64) error {
    query := "DELETE FROM products WHERE id = $1"
    _, err := r.db.Exec(query, id)
    return err
}

func (r *PostgresProductRepository) Search(searchQuery string) ([]*domain.Product, error) {
    query := `
        SELECT id, name, price, currency, category, stock, is_active, created_at, updated_at
        FROM products
        WHERE name ILIKE $1 OR category ILIKE $1
    `
    
    searchPattern := fmt.Sprintf("%%%s%%", searchQuery)
    rows, err := r.db.Query(query, searchPattern)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var products []*domain.Product
    for rows.Next() {
        var product domain.Product
        var priceAmount float64
        var priceCurrency string
        
        err := rows.Scan(
            &product.ID,
            &product.Name,
            &priceAmount,
            &priceCurrency,
            &product.Category,
            &product.Stock,
            &product.IsActive,
            &product.CreatedAt,
            &product.UpdatedAt,
        )
        
        if err != nil {
            continue
        }
        
        product.Price = domain.NewMoney(priceAmount, priceCurrency)
        products = append(products, &product)
    }
    
    return products, nil
}

📂 adapters/secondary/mongodb/

Sorumluluk: MongoDB repository adapter (alternatif implementation)

Örnek:

package mongodb

import (
    "context"
    
    "myapp/core/domain"
    "myapp/core/ports/secondary"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
)

// MongoDB Adapter - AYNI SECONDARY PORT'u implement eder
type MongoProductRepository struct {
    collection *mongo.Collection
}

func NewMongoProductRepository(db *mongo.Database) secondary.ProductRepository {
    return &MongoProductRepository{
        collection: db.Collection("products"),
    }
}

func (r *MongoProductRepository) Save(product *domain.Product) error {
    _, err := r.collection.InsertOne(context.Background(), product)
    return err
}

func (r *MongoProductRepository) FindById(id int64) (*domain.Product, error) {
    var product domain.Product
    err := r.collection.FindOne(
        context.Background(),
        bson.M{"id": id},
    ).Decode(&product)
    
    return &product, err
}

// ... diğer metodlar

📂 adapters/secondary/smtp/

Sorumluluk: Email service adapter

Örnek:

package smtp

import (
    "fmt"
    "net/smtp"
    
    "myapp/core/ports/secondary"
)

// SMTP Adapter - implements SECONDARY PORT
type SMTPEmailService struct {
    host     string
    port     int
    username string
    password string
    from     string
}

func NewSMTPEmailService(host string, port int, username, password, from string) secondary.EmailService {
    return &SMTPEmailService{
        host:     host,
        port:     port,
        username: username,
        password: password,
        from:     from,
    }
}

func (s *SMTPEmailService) SendProductCreatedNotification(productName string, to []string) error {
    subject := "New Product Created"
    body := fmt.Sprintf("A new product has been created: %s", productName)
    
    return s.sendEmail(to, subject, body)
}

func (s *SMTPEmailService) SendLowStockAlert(productName string, stock int) error {
    subject := "Low Stock Alert"
    body := fmt.Sprintf("Product %s has low stock: %d units remaining", productName, stock)
    
    return s.sendEmail([]string{"[email protected]"}, subject, body)
}

func (s *SMTPEmailService) sendEmail(to []string, subject, body string) error {
    message := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s",
        s.from, to[0], subject, body)
    
    auth := smtp.PlainAuth("", s.username, s.password, s.host)
    addr := fmt.Sprintf("%s:%d", s.host, s.port)
    
    return smtp.SendMail(addr, auth, s.from, to, []byte(message))
}

📂 adapters/secondary/redis/

Sorumluluk: Redis cache adapter

Örnek:

package redis

import (
    "context"
    "encoding/json"
    "time"
    
    "myapp/core/ports/secondary"
    "github.com/go-redis/redis/v8"
)

// Redis Adapter - implements SECONDARY PORT
type RedisCacheService struct {
    client *redis.Client
}

func NewRedisCacheService(addr string) secondary.CacheService {
    client := redis.NewClient(&redis.Options{
        Addr: addr,
    })
    
    return &RedisCacheService{client: client}
}

func (r *RedisCacheService) Get(key string) interface{} {
    val, err := r.client.Get(context.Background(), key).Result()
    if err != nil {
        return nil
    }
    
    var result interface{}
    json.Unmarshal([]byte(val), &result)
    return result
}

func (r *RedisCacheService) Set(key string, value interface{}, ttl time.Duration) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }
    
    return r.client.Set(context.Background(), key, data, ttl).Err()
}

func (r *RedisCacheService) Delete(key string) error {
    return r.client.Del(context.Background(), key).Err()
}

Gerçek Proje Örneği

📂 Tam Proje Yapısı

ecommerce-hexagonal/
├── core/
│   ├── domain/
│   │   ├── product.go
│   │   ├── order.go
│   │   ├── customer.go
│   │   ├── money.go
│   │   └── errors.go
│   │
│   ├── ports/
│   │   ├── primary/
│   │   │   ├── product_service.go
│   │   │   ├── order_service.go
│   │   │   └── customer_service.go
│   │   │
│   │   └── secondary/
│   │       ├── product_repository.go
│   │       ├── order_repository.go
│   │       ├── email_service.go
│   │       ├── payment_gateway.go
│   │       └── cache_service.go
│   │
│   └── services/
│       ├── product_service.go
│       ├── order_service.go
│       └── customer_service.go
│
├── adapters/
│   ├── primary/
│   │   ├── http/
│   │   │   ├── handlers/
│   │   │   ├── middleware/
│   │   │   ├── dto/
│   │   │   ├── router.go
│   │   │   └── server.go
│   │   │
│   │   ├── grpc/
│   │   │   ├── proto/
│   │   │   ├── generated/
│   │   │   └── services/
│   │   │
│   │   └── cli/
│   │       └── commands/
│   │
│   └── secondary/
│       ├── postgresql/
│       │   ├── product_repository.go
│       │   ├── order_repository.go
│       │   ├── migrations/
│       │   └── connection.go
│       │
│       ├── mongodb/
│       │   └── customer_repository.go
│       │
│       ├── redis/
│       │   └── cache_service.go
│       │
│       ├── smtp/
│       │   └── email_service.go
│       │
│       ├── stripe/
│       │   └── payment_gateway.go
│       │
│       └── aws-s3/
│           └── file_storage.go
│
├── config/
│   ├── config.go
│   └── config.yaml
│
├── cmd/
│   ├── http-server/
│   │   └── main.go
│   ├── grpc-server/
│   │   └── main.go
│   └── cli/
│       └── main.go
│
├── tests/
│   ├── unit/
│   ├── integration/
│   └── mocks/
│       └── mock_repositories.go
│
├── go.mod
├── go.sum
└── README.md

🚀 main.go - Dependency Injection

package main

import (
    "database/sql"
    "log"
    "net/http"
    
    // Core
    "myapp/core/services"
    
    // Primary Adapters
    httpAdapter "myapp/adapters/primary/http"
    
    // Secondary Adapters
    "myapp/adapters/secondary/postgresql"
    "myapp/adapters/secondary/redis"
    "myapp/adapters/secondary/smtp"
    
    // Config
    "myapp/config"
    
    _ "github.com/lib/pq"
)

func main() {
    // Load configuration
    cfg := config.Load()
    
    // Initialize database connection
    db, err := sql.Open("postgres", cfg.DatabaseURL)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    // Create SECONDARY adapters (driven)
    productRepo := postgresql.NewPostgresProductRepository(db)
    emailService := smtp.NewSMTPEmailService(
        cfg.SMTPHost,
        cfg.SMTPPort,
        cfg.SMTPUsername,
        cfg.SMTPPassword,
        cfg.SMTPFrom,
    )
    cacheService := redis.NewRedisCacheService(cfg.RedisAddr)
    
    // Create CORE services (inject secondary ports)
    productService := services.NewProductService(
        productRepo,      // secondary port
        emailService,     // secondary port
        cacheService,     // secondary port
    )
    
    // Create PRIMARY adapters (driving)
    router := httpAdapter.SetupRoutes(productService)  // primary port
    
    // Start HTTP server
    log.Printf("Server starting on port %s", cfg.HTTPPort)
    log.Fatal(http.ListenAndServe(":"+cfg.HTTPPort, router))
}

Best Practices

✅ DO (Yapılması Gerekenler)

1. Port'ları Core'da Tanımlayın

// ✅ DOĞRU - Port core'da
// core/ports/secondary/product_repository.go
package secondary

type ProductRepository interface {
    Save(product *domain.Product) error
}
// ❌ YANLIŞ - Port adapter'da
// adapters/secondary/postgresql/product_repository.go
type ProductRepository interface {
    Save(product *domain.Product) error
}

2. Core'u Bağımsız Tutun

// ✅ DOĞRU - Core hiçbir framework'e bağımlı değil
package domain

type Product struct {
    ID    int64
    Name  string
    Price Money
}
// ❌ YANLIŞ - Core framework'e bağımlı
package domain

import "gorm.io/gorm"

type Product struct {
    gorm.Model  // ❌ GORM'a bağımlı!
    Name  string
    Price float64
}

3. Dependency Injection Kullanın

// ✅ DOĞRU - Constructor injection
func NewProductService(repo secondary.ProductRepository) primary.ProductService {
    return &ProductService{repo: repo}
}
// ❌ YANLIŞ - Hard-coded dependency
type ProductService struct {
    repo *PostgresProductRepository  // ❌ Concrete type'a bağımlı
}

4. DTO Kullanın (Data Transfer Objects)

// ✅ DOĞRU - HTTP DTO ayrı, Domain entity ayrı
type CreateProductRequest struct {  // HTTP DTO
    Name  string `json:"name"`
    Price float64 `json:"price"`
}

type Product struct {  // Domain entity
    ID    int64
    Name  string
    Price Money
}

5. Test için Mock Adapter Oluşturun

// tests/mocks/mock_product_repository.go
type MockProductRepository struct {
    products map[int64]*domain.Product
}

func (m *MockProductRepository) Save(product *domain.Product) error {
    m.products[product.ID] = product
    return nil
}

// Test
func TestCreateProduct(t *testing.T) {
    // Mock repository kullan
    mockRepo := &MockProductRepository{products: make(map[int64]*domain.Product)}
    service := services.NewProductService(mockRepo, nil, nil)
    
    // Test
    product, err := service.CreateProduct("Test", domain.NewMoney(100, "USD"), "Tech")
    assert.NoError(t, err)
    assert.NotNil(t, product)
}

❌ DON'T (Yapılmaması Gerekenler)

1. Core'dan Adapter'a Bağımlılık

// ❌ YANLIŞ - Core, adapter'ı import ediyor
package services

import "myapp/adapters/secondary/postgresql"  // ❌

type ProductService struct {
    repo *postgresql.PostgresProductRepository  // ❌
}

2. Business Logic in Adapters

// ❌ YANLIŞ - İş mantığı adapter'da
func (h *HTTPHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
    // ❌ Validation burada olmamalı
    if req.Price < 0 {
        return errors.New("invalid price")
    }
    
    // ❌ Business rule burada olmamalı
    if req.Discount > 70 {
        return errors.New("discount too high")
    }
}

3. Port Bypass

// ❌ YANLIŞ - Adapter direkt başka adapter'ı çağırıyor
type HTTPHandler struct {
    repo *postgresql.PostgresProductRepository  // ❌ Port yerine direkt adapter
}

Avantajlar ve Dezavantajlar

✅ Avantajlar

1. Teknoloji Bağımsızlığı

PostgreSQL → MongoDB    (sadece adapter değişir)
REST API → gRPC        (sadece adapter değişir)
SMTP → SendGrid        (sadece adapter değişir)

2. Test Edilebilirlik

// Production
productService := services.NewProductService(
    postgresRepo,
    smtpEmail,
    redisCache,
)

// Test
productService := services.NewProductService(
    mockRepo,
    mockEmail,
    mockCache,
)

3. Değiştirilebilirlik

  • Database değişirse → Sadece repository adapter değişir
  • Email provider değişirse → Sadece email adapter değişir
  • Core ve diğer adapter'lar etkilenmez

4. Paralel Geliştirme

  • Frontend team: HTTP adapter'ı geliştirir
  • Backend team: Core service'i geliştirir
  • Database team: Repository adapter'ı geliştirir

5. Business Logic Koruması

  • Core dış dünyadan izole
  • Framework değişikliği core'u etkilemez
  • Business rules bir yerde toplanmış

❌ Dezavantajlar

1. Öğrenme Eğrisi

  • Port/Adapter kavramını anlamak zaman alır
  • Yeni geliştiriciler için karmaşık olabilir

2. Boilerplate Kod

  • Çok sayıda interface
  • Adapter mapping kod tekrarı
  • DTO ↔ Domain dönüşümleri

3. Over-Engineering Riski

  • Basit CRUD app için gereksiz
  • Küçük projeler için fazla karmaşık

4. İlk Geliştirme Yavaş

  • Setup süresi uzun
  • Interface'ler ve adapter'lar hazırlamak zaman alır

Ne Zaman Kullanmalı?

✅ Hexagonal Architecture Uygun

  • ✅ Long-lived projeler (5+ yıl)
  • ✅ Sık değişen gereksinimler
  • ✅ Multiple client'lar (Web, Mobile, API)
  • ✅ Test coverage önemli
  • ✅ Teknoloji değişikliği olasılığı yüksek
  • ✅ Complex business logic
  • ✅ Enterprise uygulamalar

❌ Hexagonal Architecture Gereksiz

  • ❌ Simple CRUD apps
  • ❌ Prototype/MVP
  • ❌ Kısa ömürlü projeler
  • ❌ Tek geliştirici
  • ❌ Fixed requirements
  • ❌ Minimal business logic

Özet

🎯 Hexagonal Architecture Formülü

1. CORE (İçerdeki Altıgen)
   ├── Domain Entities (Pure business objects)
   ├── Ports (Interfaces)
   └── Services (Use cases)

2. PRIMARY ADAPTERS (Dışarıdan İçeriye)
   ├── HTTP REST API
   ├── gRPC Server
   ├── GraphQL
   └── CLI

3. SECONDARY ADAPTERS (İçeriden Dışarıya)
   ├── PostgreSQL Repository
   ├── MongoDB Repository
   ├── Redis Cache
   ├── SMTP Email
   └── Stripe Payment

4. DEPENDENCY DIRECTION
   Adapters → Ports → Core
   (Bağımlılık daima içeri doğru)

🔑 Anahtar Noktalar

  1. Core hiçbir şeye bağımlı değil
  2. Port'lar = Interface'ler
  3. Adapter'lar = Implementation'lar
  4. Primary = Uygulamayı kullanan
  5. Secondary = Uygulama tarafından kullanılan
  6. Dependency Injection zorunlu
  7. Test için mock adapter'lar

📚 Daha Fazla Öğrenmek İçin

  • Alistair Cockburn - Hexagonal Architecture orijinal makale
  • Robert C. Martin - Clean Architecture (benzer konseptler)
  • Domain-Driven Design - Eric Evans
  • Ports and Adapters - Wikipedia

Hexagonal Architecture, doğru kullanıldığında sürdürülebilir, test edilebilir ve esnek uygulamalar geliştirmenizi sağlar! 🚀