🔷 Hexagonal Architecture (Ports & Adapters)
📚 İçindekiler
- Hexagonal Architecture Nedir?
- Neden Hexagonal Architecture?
- Temel Kavramlar
- Klasör Yapısı
- Detaylı Açıklamalar
- Gerçek Proje Örneği
- Best Practices
- 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
- Core hiçbir şeye bağımlı değil
- Port'lar = Interface'ler
- Adapter'lar = Implementation'lar
- Primary = Uygulamayı kullanan
- Secondary = Uygulama tarafından kullanılan
- Dependency Injection zorunlu
- 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! 🚀