Interfaces Implícitas vs Explícitas en Go: Cómo la Magia se Convierte en Pesadilla
Una guía profunda sobre las interfaces implícitas en Go, por qué es peligroso en arquitectura hexagonal, cómo Go te permite escribir 'hexagonal sin pensarlo', y por qué eso rompe la arquitectura. Con ejemplos de desastre real y patrones para hacerlo bien.
Existe una característica de Go que es simultáneamente su mayor fortaleza y su trampa más peligrosa: las interfaces implícitas.
La característica es simple: en Go, no necesitas declarar explícitamente que tu tipo implementa una interfaz. Si tienes los métodos correctos, automáticamente la implementas. Es hermoso. Es elegante. Y es exactamente lo que te permite escribir código que parece seguir arquitectura hexagonal cuando en realidad está roto.
He visto esta trampa una y otra vez. Equipos que creen tener una arquitectura hexagonal limpia, con puertos y adapters bien definidos, solo para descubrir meses después que sus “puertos” en realidad están completamente acoplados, que sus “adapters” dependen de otros adapters, y que el sistema es casi tan rígido como si hubieran escrito código monolítico desde el inicio.
La ironía es que Go te lo permitió sin quejarse. Compiló perfectamente. Los tests pasaron. Nadie en el equipo se dio cuenta de que habían caminado directamente a una trampa arquitectónica.
Este artículo es una exploración profunda de por qué las interfaces implícitas de Go son tan peligrosas en el contexto de arquitectura hexagonal. Explicaré qué son, por qué te permiten escribir código que parece hexagonal cuando no lo es, y cómo identificar y evitar esta trampa. Con ejemplos de desastre real, patrones para hacerlo bien, y un checklist que puedes usar ahora mismo en tu codebase.
Parte 1: Las Interfaces Implícitas y Su Magia
1.1 ¿Qué Son Las Interfaces Implícitas?
Una interfaz implícita en Go significa que no necesitas declarar que implementas una interfaz. Si tu tipo tiene los métodos correctos, automáticamente satisface la interfaz.
Ejemplo simple:
// La interfaz
type Writer interface {
Write(p []byte) (n int, err error)
}
// Tu tipo
type MyLogger struct{}
func (ml MyLogger) Write(p []byte) (int, error) {
fmt.Println(string(p))
return len(p), nil
}
// MyLogger automáticamente es un Writer
// No necesitaste escribir "implements Writer" en ningún lado
Puedes pasarle un MyLogger a cualquier función que espera un Writer:
func SaveToFile(w Writer, data []byte) {
w.Write(data)
}
func main() {
logger := MyLogger{}
SaveToFile(logger, []byte("hello"))
// Funciona perfectamente
}
Esto es diferente a Java o C#, donde debes declarar explícitamente:
// Java requiere declaración explícita
public class MyLogger implements Writer {
// ...
}
1.2 Por Qué Esto Es Hermoso
La característica de interfaces implícitas es increíblemente poderosa. Permite:
1. Desacoplamiento natural:
// Tu código no depende de una interfaz específica
// Simplemente satisface lo que necesita
type MyService struct{}
func (ms MyService) Log(msg string) {
fmt.Println(msg)
}
// Meses después, alguien crea una interfaz Logger
type Logger interface {
Log(msg string)
}
// Y mágicamente, MyService ya la implementa
// Sin cambiar una línea de código
2. Pequeñas interfaces:
Go te anima a crear interfaces pequeñas (1-3 métodos), porque satisfacerlas es trivial. En Java, crearías una interfaz monolítica con 20 métodos.
3. Adopción retroactiva:
Si trabajas con librerías de terceros, puedes crear interfaces que sus tipos ya satisfacen:
type Reader interface {
Read(p []byte) (int, error)
}
// La librería de terceros tiene un MyType con método Read
// Nunca planeó ser un Reader
// Pero automáticamente lo es
myType := thirdparty.NewMyType()
useAsReader(myType) // Funciona
1.3 Comparación: Go vs Java vs Python
Go (implícito):
type Reader interface {
Read(p []byte) (int, error)
}
type MyType struct{}
func (mt MyType) Read(p []byte) (int, error) { ... }
// MyType ya es un Reader
Java (explícito):
public interface Reader {
int read(byte[] p) throws IOException;
}
public class MyType implements Reader {
public int read(byte[] p) throws IOException { ... }
}
// MyType debe declarar que implementa Reader
Python (duck typing):
class Reader:
def read(self, p: bytes) -> int:
pass
class MyType:
def read(self, p: bytes) -> int:
pass
# Python no tiene interfaces, confía en que tengas los métodos
# Si no los tienes, falla en runtime
Go es el punto dulce: tiene la flexibilidad de Python (no necesitas declaración) con la seguridad de Java (el compilador verifica tipos en tiempo de compilación).
O eso es lo que parece…
Parte 2: El Peligro Oculto en Hexagonal
2.1 El Problema: Demasiada Libertad
Aquí viene el twist: la facilidad de crear interfaces implícitas en Go te permite escribir código que parece seguir arquitectura hexagonal cuando en realidad está completamente roto.
Y lo peor es que el compilador no te lo dirá. Tu código compilará perfectamente. Tus tests pasarán. Y solo cuando necesites cambiar algo, descubrirás que tu arquitectura es frágil.
2.2 Escenario de Desastre: Adapters Acoplados
Imagina que estás construyendo un servicio que maneja órdenes. Tomas las mejores prácticas de hexagonal y empiezas así:
// Dominio
type Order struct {
ID string
Total float64
}
// Puerto: Persistencia
type OrderRepository interface {
Save(order *Order) error
GetByID(id string) (*Order, error)
}
// Adapter: PostgreSQL
type PostgresOrderRepository struct {
db *sql.DB
}
func (p *PostgresOrderRepository) Save(order *Order) error {
// Guardar en Postgres
_, err := p.db.Exec("INSERT INTO orders...", order.ID, order.Total)
return err
}
func (p *PostgresOrderRepository) GetByID(id string) (*Order, error) {
// Obtener de Postgres
row := p.db.QueryRow("SELECT * FROM orders WHERE id = $1", id)
var order Order
row.Scan(&order.ID, &order.Total)
return &order, nil
}
Hasta aquí, perfecto. Ahora necesitas notificaciones por email cuando se crea una orden. Creas un adapter para email:
// Adapter: Email
type EmailService struct {
smtp *mail.SMTP
}
func (e *EmailService) SendOrderConfirmation(order *Order) error {
// Enviar email
msg := fmt.Sprintf("Tu orden #%s por $%.2f ha sido creada", order.ID, order.Total)
return e.smtp.Send("user@example.com", msg)
}
Perfecto. Ahora crean un caso de uso para crear órdenes:
// Caso de Uso
type CreateOrderUseCase struct {
repo OrderRepository
email *EmailService
}
func (c *CreateOrderUseCase) Execute(order *Order) error {
if err := c.repo.Save(order); err != nil {
return err
}
// Aquí está el problema
if err := c.email.SendOrderConfirmation(order); err != nil {
// ¿Qué hacemos si falla el email?
// ¿Revertimos la orden?
}
return nil
}
¿Ves el problema?
El caso de uso está acoplado directamente a EmailService. No a una interfaz Notifier. A la implementación específica de email.
Peor aún, ¿qué pasa cuando necesitas SMS también? ¿Teléfono? ¿Notificación push?
// Esto es lo que termina pasando
type CreateOrderUseCase struct {
repo OrderRepository
email *EmailService
sms *SMSService
push *PushNotificationService
slack *SlackNotificationService
// ... 10 más
}
Ahora tienes un monolito disfrazado de arquitectura hexagonal.
¿Por qué pasó esto?
Porque fue demasiado fácil crear el acoplamiento. En Go, simplemente agregaste email *EmailService a la estructura, y compiló. No había interfaz, no había puerto. Solo lo olvidaste.
En Java, si intentas esto:
public class CreateOrderUseCase {
private EmailService email; // Error: EmailService no es una interfaz
}
Java te fuerza a crear una interfaz. Y al crear la interfaz, empiezas a pensar: “¿Realmente quiero que el caso de uso dependa de una interfaz para notificaciones?”
Go no te fuerza. Y por eso es peligroso.
2.3 Patrón de Desastre: Interfaces Accidentales
Aquí hay un patrón que veo constantemente:
Paso 1: Crear adapters específicos
type PaymentProcessor struct { ... }
type NotificationService struct { ... }
type LoggingService struct { ... }
Paso 2: Importarlos en el caso de uso
type OrderService struct {
payments *PaymentProcessor
notifications *NotificationService
logging *LoggingService
}
Paso 3: Meses después, alguien intenta testear
func TestCreateOrder(t *testing.T) {
svc := OrderService{
payments: &PaymentProcessor{
gateway: realPaymentGateway, // Oops, necesitas acceso real
},
notifications: &NotificationService{
smtp: realSMTPConnection, // Oops, necesitas servidor real
},
logging: &LoggingService{
file: openFile(), // Oops, necesitas archivo real
},
}
// Para testear, necesitas todas las dependencias reales
// Los tests son lentos y frágiles
}
Paso 4: Alguien se da cuenta y agrega interfaces
type PaymentGateway interface { ... }
type Notifier interface { ... }
type Logger interface { ... }
type OrderService struct {
payments PaymentGateway
notifications Notifier
logging Logger
}
Pero aquí está el problema: El daño ya está hecho. Ahora tienes todas las dependencias acopladas en el caso de uso. Las interfaces son solo un band-aid.
Parte 3: Interfaces Explícitas Como Defensa
3.1 El Antídoto: Ser Explícito Sobre Intenciones
La solución es pensar explícitamente sobre qué interfaces realmente necesitas, antes de crear un tipo.
En lugar de:
// ❌ Implícito: ¿Es esto un puerto?
type EmailService struct {
smtp *mail.SMTP
}
func (e *EmailService) SendOrderConfirmation(order *Order) error {
// ...
}
Haz esto:
// ✅ Explícito: Define el puerto primero
type OrderNotifier interface {
NotifyOrderCreated(order *Order) error
}
// Ahora, la implementación por email
type EmailNotifier struct {
smtp *mail.SMTP
}
func (e *EmailNotifier) NotifyOrderCreated(order *Order) error {
msg := fmt.Sprintf("Tu orden #%s ha sido creada", order.ID)
return e.smtp.Send("user@example.com", msg)
}
// Y puedes tener otras implementaciones
type SlackNotifier struct {
webhook string
}
func (s *SlackNotifier) NotifyOrderCreated(order *Order) error {
return s.postToSlack(fmt.Sprintf("Nueva orden: %s", order.ID))
}
// El caso de uso SOLO depende de la interfaz, no de implementaciones
type CreateOrderUseCase struct {
repo OrderRepository
notifier OrderNotifier
}
func (c *CreateOrderUseCase) Execute(order *Order) error {
if err := c.repo.Save(order); err != nil {
return err
}
// Usa la interfaz, no la implementación
return c.notifier.NotifyOrderCreated(order)
}
¿Qué cambió?
- La interfaz existe primero:
OrderNotifieres un puerto claro - Las implementaciones la satisfacen:
EmailNotifier,SlackNotifier - El caso de uso depende de la interfaz: No está acoplado a email específicamente
- Puedes cambiar implementaciones fácilmente: Agregar SMS es solo crear
SMSNotifier
3.2 Patrón: Definir Puertos Antes que Adapters
Aquí hay un checklist para saber si estás siendo explícito:
Antes de crear un adapter, pregúntate:
- ¿Cuál es el puerto (interfaz) que necesito?
- ¿Cuál es el contrato que esta interfaz establece?
- ¿Cómo sé que esta interfaz es realmente un puerto y no solo “una interfaz”?
El patrón correcto:
// PASO 1: Definir el puerto (interfaz)
// Este puerto vive en el dominio o en application
type UserRepository interface {
GetByID(id string) (*User, error)
Save(user *User) error
Delete(id string) error
}
// PASO 2: Implementar el adapter
// Este adapter vive en adapter/repository
type PostgresUserRepository struct {
db *sql.DB
}
func (p *PostgresUserRepository) GetByID(id string) (*User, error) {
// Implementación específica de Postgres
row := p.db.QueryRow("SELECT * FROM users WHERE id = $1", id)
var user User
row.Scan(&user.ID, &user.Name)
return &user, nil
}
// PASO 3: El caso de uso usa la interfaz, no la implementación
type GetUserUseCase struct {
repo UserRepository // ← Interfaz, no *PostgresUserRepository
}
func (u *GetUserUseCase) Execute(id string) (*User, error) {
return u.repo.GetByID(id)
}
3.3 El Patrón del “Fake” para Verificar Puertos
Aquí hay un patrón potente para verificar que tu interfaz es realmente un puerto:
Crea una implementación “fake” sin dependencias externas:
// Fake implementation para testing
type FakeUserRepository struct {
users map[string]*User
}
func (f *FakeUserRepository) GetByID(id string) (*User, error) {
user, exists := f.users[id]
if !exists {
return nil, errors.New("not found")
}
return user, nil
}
func (f *FakeUserRepository) Save(user *User) error {
f.users[user.ID] = user
return nil
}
func (f *FakeUserRepository) Delete(id string) error {
delete(f.users, id)
return nil
}
// Ahora testea sin base de datos
func TestGetUser(t *testing.T) {
repo := &FakeUserRepository{
users: map[string]*User{
"1": {ID: "1", Name: "John"},
},
}
useCase := &GetUserUseCase{repo: repo}
user, err := useCase.Execute("1")
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
Si puedes crear un Fake fácilmente, el puerto está bien diseñado.
Si es difícil hacer un Fake, significa la interfaz probablemente mezcla responsabilidades.
Parte 4: El Lado Oscuro - Cómo Go Te Engaña
4.1 La Ilusión de Satisfacción
Aquí hay algo sutil pero peligroso. Go te permite hacer esto:
// Defines una interfaz
type Reader interface {
Read(p []byte) (int, error)
}
// Creas un tipo que no está intentando satisfacerla
type MyType struct {
data []byte
}
func (m *MyType) Read(p []byte) (int, error) {
copy(p, m.data)
return len(m.data), nil
}
// Y luego, sin darte cuenta, usas MyType donde se espera Reader
func ProcessData(r Reader, data []byte) {
r.Read(data)
}
func main() {
mt := &MyType{data: []byte("hello")}
ProcessData(mt, make([]byte, 10)) // Funciona
}
El peligro: Nunca tuviste la intención de que MyType fuera un Reader. Solo pasó que tenía ese método. Y ahora está acoplado.
4.2 Accidente Arquitectónico
Imagina este escenario real:
// Crear varias cosas que tienes métodos Log
type PaymentGateway struct{}
func (p *PaymentGateway) Log(msg string) { fmt.Println("[Payment]", msg) }
type EmailService struct{}
func (e *EmailService) Log(msg string) { fmt.Println("[Email]", msg) }
type OrderService struct{}
func (o *OrderService) Log(msg string) { fmt.Println("[Order]", msg) }
// Sin darte cuenta, todos satisfacen esta interfaz implícitamente
type Logger interface {
Log(msg string)
}
// Y ahora podrías pasar cualquiera donde se espera Logger
// Lo cual es completamente incorrecto
Nadie planificó esto. Sucedió por accidente. Y es exactamente lo que hace que las interfaces implícitas sean peligrosas.
4.3 El Test Falso Negativo
Aquí hay un ejemplo de cómo Go te puede engañar:
// Tu código "parece" bien
type CreateOrderUseCase struct {
repo OrderRepository // ✅ Puerto
notifier OrderNotifier // ✅ Puerto
}
// Pero en realidad hiciste esto
type CreateOrderUseCase struct {
repo OrderRepository // ✅ Puerto
notifier OrderNotifier // Espera... es esto un puerto?
stripe *stripe.Client // ❌ Acoplamiento directo
database *sql.DB // ❌ Acoplamiento directo
}
Y el compilador no se queja porque OrderNotifier es una interfaz.
Parte 5: Cómo Identificar La Trampa en Tu Código
5.1 Checklist: ¿Es Esto Un Puerto Real?
Para cada interfaz en tu código de casos de uso, pregúntate:
1. ¿Definí esta interfaz explícitamente?
// ✅ Bien
type UserRepository interface { ... }
// ❌ Malo: Solo agregaste un campo de tipo que "parece" una interfaz
type UserService struct {
repo *PostgresUserRepository
}
2. ¿El nombre de la interfaz describe QUÉ hace, no CÓMO lo hace?
// ✅ Bien: Describe qué hace
type UserRepository interface {
GetByID(id string) (*User, error)
}
// ❌ Malo: Describe cómo lo hace
type PostgresUserRepository interface {
QueryByID(id string) (*User, error)
}
3. ¿La interfaz tiene 1-3 métodos?
// ✅ Bien: Pequeña, coherente
type Reader interface {
Read(p []byte) (int, error)
}
// ❌ Malo: Hace demasiadas cosas
type Database interface {
Query(...)
Exec(...)
Create(...)
Read(...)
Update(...)
Delete(...)
// ... 20 métodos más
}
4. ¿Puedo crear una implementación “fake” sin dependencias?
// ✅ Bien: Fácil de hacer fake
type FakeRepository struct {
data map[string]*User
}
// ❌ Malo: Necesita dependencias externas incluso para fake
type FakeRepository struct {
db *sql.DB // Aún necesitas una BD
}
5. ¿Si cambio la implementación, el dominio se ve afectado?
// ✅ Bien: El dominio no se ve afectado
// Cambiar de PostgreSQL a MongoDB: solo cambias el adapter
// ❌ Malo: El dominio sabe de la implementación
type User struct {
postgresID string // ← El dominio sabe de PostgreSQL
}
5.2 Análisis de Código: Encontrando Acoplamiento Oculto
Ejecuta este análisis en tu codebase:
Paso 1: Busca todos los struct en usecase
grep -r "type.*struct" internal/usecase/
Paso 2: Para cada struct, lista sus campos
type CreateOrderUseCase struct {
repo OrderRepository // Campo 1
notifier OrderNotifier // Campo 2
stripe *stripe.Client // ¿Es esto un puerto?
}
Paso 3: Para cada campo, pregunta: ¿Es una interfaz o una implementación?
// Si el tipo empieza con un nombre concreto: *stripe.Client
// Probablemente es acoplamiento directo
// Si el tipo es una interfaz: OrderNotifier
// Probablemente es un puerto
Paso 4: Busca campos que no son interfaces
grep -A 20 "type.*UseCase struct" internal/usecase/*.go | grep -v "interface"
Si encuentras campos como:
*postgres.DB*stripe.Client*smtp.Connection*firebase.App
Tienes acoplamiento.
Parte 6: Patrones Para Hacerlo Bien
6.1 El Patrón “Puerto Primero”
Siempre define el puerto antes de la implementación:
// 📁 internal/domain/repository.go
// O en internal/usecase si no es crítico de negocio
// PASO 1: Define el puerto
type UserRepository interface {
GetByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
}
// 📁 internal/adapter/repository/postgres.go
// PASO 2: Implementa el adapter
type PostgresUserRepository struct {
db *sql.DB
}
func (p *PostgresUserRepository) GetByID(ctx context.Context, id string) (*User, error) {
row := p.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
var user User
if err := row.Scan(&user.ID, &user.Name); err != nil {
return nil, err
}
return &user, nil
}
// 📁 internal/usecase/create_user.go
// PASO 3: El caso de uso usa la interfaz
type CreateUserUseCase struct {
repo UserRepository
}
func (c *CreateUserUseCase) Execute(ctx context.Context, user *User) error {
return c.repo.Save(ctx, user)
}
Estructura de carpetas:
internal/
├── domain/
│ ├── user.go # Entidades del dominio
│ └── repository.go # Puertos (interfaces)
├── usecase/
│ └── create_user.go # Casos de uso
├── adapter/
│ ├── repository/
│ │ └── postgres.go # Implementación del puerto
│ └── http/
│ └── user_handler.go # Entrada HTTP
6.2 El Patrón de “Mockeo Explícito”
En lugar de confiar en satisfacción implícita, crea mocks explícitos:
// ✅ Bien: Mock explícito para testing
type MockUserRepository struct {
GetByIDFunc func(ctx context.Context, id string) (*User, error)
SaveFunc func(ctx context.Context, user *User) error
}
func (m *MockUserRepository) GetByID(ctx context.Context, id string) (*User, error) {
if m.GetByIDFunc != nil {
return m.GetByIDFunc(ctx, id)
}
return nil, errors.New("not implemented")
}
func (m *MockUserRepository) Save(ctx context.Context, user *User) error {
if m.SaveFunc != nil {
return m.SaveFunc(ctx, user)
}
return errors.New("not implemented")
}
// Test explícito
func TestCreateUser(t *testing.T) {
mock := &MockUserRepository{
SaveFunc: func(ctx context.Context, user *User) error {
return nil // Comportamiento esperado
},
}
useCase := &CreateUserUseCase{repo: mock}
err := useCase.Execute(context.Background(), &User{})
assert.NoError(t, err)
}
6.3 El Patrón de “Constructor Explícito”
En lugar de pasar tipos directamente, usa constructores que dejan claro qué inyectas:
// ❌ Confuso: ¿Qué parámetros son puertos?
type OrderService struct {
a *TypeA
b *TypeB
c *TypeC
}
// ✅ Claro: Constructor explícito
func NewOrderService(
repo OrderRepository, // ← Puerto
notifier OrderNotifier, // ← Puerto
logger Logger, // ← Puerto
) *OrderService {
return &OrderService{
repo: repo,
notifier: notifier,
logger: logger,
}
}
// Ahora es obvio qué son puertos
service := NewOrderService(postgresRepo, emailNotifier, zeroLogger)
Parte 7: Ejemplos de Desastre vs Corrección
7.1 Ejemplo 1: Payment Processing
❌ MALO: Acoplamiento implícito
type ProcessPaymentUseCase struct {
stripe *stripe.Client // ❌ Acoplado a Stripe
db *sql.DB // ❌ Acoplado a SQL
email *mail.SMTP // ❌ Acoplado a SMTP
}
func (p *ProcessPaymentUseCase) Execute(payment *Payment) error {
// ¿Qué pasa si quiero cambiar a Paypal?
// ¿Qué pasa si quiero cambiar a MongoDB?
// El caso de uso está lleno de detalles técnicos
return p.stripe.Charge(payment.Amount)
}
✅ BIEN: Puertos explícitos
// Puertos
type PaymentGateway interface {
Charge(ctx context.Context, amount float64) (transactionID string, err error)
}
type PaymentRepository interface {
Save(ctx context.Context, payment *Payment) error
}
type PaymentNotifier interface {
NotifyPaymentProcessed(ctx context.Context, payment *Payment) error
}
// Caso de Uso
type ProcessPaymentUseCase struct {
gateway PaymentGateway
repo PaymentRepository
notifier PaymentNotifier
}
func (p *ProcessPaymentUseCase) Execute(ctx context.Context, payment *Payment) error {
// Solo depende de interfaces
transactionID, err := p.gateway.Charge(ctx, payment.Amount)
if err != nil {
return err
}
payment.TransactionID = transactionID
if err := p.repo.Save(ctx, payment); err != nil {
return err
}
return p.notifier.NotifyPaymentProcessed(ctx, payment)
}
// Implementaciones (en adapter/)
type StripePaymentGateway struct { ... }
type PaypalPaymentGateway struct { ... }
type PostgresPaymentRepository struct { ... }
type EmailPaymentNotifier struct { ... }
Ahora, cambiar de Stripe a Paypal es trivial:
// Hoy
gateway := &StripePaymentGateway{client: stripeClient}
// Mañana
gateway := &PaypalPaymentGateway{client: paypalClient}
// El resto del código sin cambios
7.2 Ejemplo 2: User Management
❌ MALO:
type UserService struct {
db *sql.DB
cache *redis.Client
validator *govalidator.StringValidator
hasher *bcrypt.Hasher
}
func (u *UserService) Register(email string, password string) error {
// Todo acoplado a implementaciones específicas
if err := u.validator.ValidateEmail(email); err != nil {
return err
}
hashed, err := u.hasher.Hash(password)
if err != nil {
return err
}
// ...
}
✅ BIEN:
// Puertos de negocio
type EmailValidator interface {
IsValid(email string) bool
}
type PasswordHasher interface {
Hash(password string) (string, error)
Verify(password, hash string) bool
}
type UserRepository interface {
Save(ctx context.Context, user *User) error
GetByEmail(ctx context.Context, email string) (*User, error)
}
// Caso de Uso
type RegisterUserUseCase struct {
emailValidator EmailValidator
passwordHasher PasswordHasher
userRepo UserRepository
}
func (r *RegisterUserUseCase) Execute(ctx context.Context, email, password string) error {
if !r.emailValidator.IsValid(email) {
return errors.New("invalid email")
}
hash, err := r.passwordHasher.Hash(password)
if err != nil {
return err
}
user := &User{Email: email, PasswordHash: hash}
return r.userRepo.Save(ctx, user)
}
Parte 8: Checklist Final - ¿Tu Arquitectura Es Realmente Hexagonal?
Usa este checklist para verificar que tus puertos son realmente explícitos:
🔍 Examen de Puertos:
- ¿Definí explícitamente cada interfaz que mi caso de uso necesita?
- ¿Cada interfaz tiene un nombre que describe QUÉ hace, no CÓMO?
- ¿Cada interfaz tiene 1-3 métodos (no más)?
- ¿Puedo crear un Fake implementation sin dependencias externas?
- ¿Las implementaciones viven en
adapter/o carpeta específica? - ¿Los casos de uso reciben interfaces, no tipos concretos?
- ¿Mi dominio no importa nada de
adapter/? - ¿Si cambio de PostgreSQL a MongoDB, cambio solo el adapter?
🔴 Red Flags (Señales de Peligro):
- ¿Un caso de uso recibe
*stripe.Clientdirectamente? - ¿Un caso de uso recibe
*sql.DBdirectamente? - ¿Un caso de uso recibe
*redis.Clientdirectamente? - ¿Tengo métodos Log/Debug/Info que aceptan tipos específicos?
- ¿Tengo campos que son “adapters” pero no en la carpeta
adapter/?
Si contestas “sí” a cualquier red flag, tienes acoplamiento.
Conclusión: La Responsabilidad Es Tuya
Go te da libertad. Las interfaces implícitas significan que puedes escribir código hermoso, desacoplado, y flexible. O puedes escribir código que parece desacoplado pero que en realidad está monolíticamente acoplado.
La diferencia está en ser consciente de lo que estás haciendo.
No confíes en la satisfacción implícita. Define tus puertos explícitamente. Nómbralos claramente. Colócalos donde pertenecen. Y crea implementaciones que realmente satisfacen esos puertos.
La facilidad de Go no es una excusa para ser perezoso con la arquitectura. Es una oportunidad para ser excelente.
Tags
Artículos relacionados
API Versioning Strategies: Cómo Evolucionar APIs sin Romper Clientes
Una guía exhaustiva sobre estrategias de versionado de APIs: URL versioning vs Header versioning, cómo deprecar endpoints sin shock, migration patterns reales, handling de cambios backwards-incompatibles, y decisiones arquitectónicas que importan. Con 50+ ejemplos de código en Go.
Arquitectura de software: Más allá del código
Una guía completa sobre arquitectura de software explicada en lenguaje humano: patrones, organización, estructura y cómo construir sistemas que escalen con tu negocio.
Automatizando tu vida con Go CLI: Guía profesional para crear herramientas de línea de comandos escalables
Una guía exhaustiva y paso a paso sobre cómo crear herramientas CLI escalables con Go 1.25.5: desde lo básico hasta proyectos empresariales complejos con flags, configuración, logging, y ejemplos prácticos para Windows y Linux.