Skip to main content

Inicia sesión en CleanKata

Sigue tu progreso, gana XP y desbloquea todas las lecciones.

Al iniciar sesión aceptas nuestros Términos de uso y Política de privacidad.

Arquitectura Limpia60 XP6 min

Estrategias de Organización: Paquete por Funcionalidad

Agrupa el código por funcionalidad de dominio — los paquetes de nivel superior revelan el negocio, cada funcionalidad está co-ubicada y la búsqueda mejora drásticamente a medida que el sistema crece.

Por qué importa

Package by Feature organiza el código de modo que la estructura de directorios revela qué hace el sistema. En lugar de controllers/, services/, repositories/, el nivel superior dice orders/, users/, payments/. Esto es lo que Robert Martin llama Arquitectura que Grita — la estructura anuncia el dominio de negocio, no la pila tecnológica.

Ventajas: Todo el código relacionado con Pedidos vive junto. Encontrar "¿cómo funciona el descuento de pedidos?" significa abrir un directorio en lugar de tres. Eliminar una funcionalidad significa eliminar un directorio. Para un nuevo desarrollador: "¿dónde vive el checkout?" — en checkout/.

La limitación: Package by Feature mejora la detectabilidad pero no aplica fronteras. Dentro de orders/, un controlador aún puede importar directamente un repositorio de users/. Los paquetes de funcionalidades pueden enredarse entre sí a través de importaciones ocultas. La estructura es mejor, pero la disciplina debe agregarse por separado — a través de modificadores de acceso, archivos barrel o puertos explícitos.

✗El problema

Feature packages exist but boundaries are violated. orders/ reaches into users/ internals, bypassing the public API and creating hidden cross-feature coupling.

Bad

# orders/order_controller.py
from orders.order_service   import OrderService
from users.user_repository  import UserRepository  # ← crosses feature boundary!

class OrderController:
    def place_order(self):
        data    = request.json
        user_id = data["user_id"]

        # Reaches INTO the users/ package internals — bypasses UserService
        user_repo        = UserRepository()
        user             = user_repo.find_by_id(user_id)
        shipping_address = user.shipping_address  # raw entity from another feature

        return jsonify(OrderService().place(data, shipping_address))

# UserRepository is now coupled to OrderController.
# Refactoring UserRepository breaks OrderController — invisible dependency.
// orders/OrderController.ts
import { UserRepository } from "../users/UserRepository"; // ← crosses boundary!
import { OrderService }   from "./OrderService";

export class OrderController {
  async placeOrder(req: { body: { userId: string } }): Promise {
    const user    = await new UserRepository().findById(req.body.userId);
    const address = user.shippingAddress; // raw entity — bypasses UserService

    return new OrderService().place(req.body, address);
  }
}

// UserRepository is now coupled to OrderController.
// Refactoring UserRepository breaks OrderController — invisible dependency.

✓La solución

Feature packages expose only a public service API. Internals (like UserRepository) are never exported outside the package. Cross-feature communication uses the public API only.

Good

# users/user_service.py  ← public API of the users/ feature
from dataclasses import dataclass
from users._user_repository import UserRepository  # private, underscore prefix

@dataclass
class ShippingAddress:
    street: str
    city: str
    country: str

class UserService:
    def get_shipping_address(self, user_id: str) -> ShippingAddress:
        repo = UserRepository()
        user = repo.find_by_id(user_id)
        return ShippingAddress(street=user.street, city=user.city, country=user.country)

# orders/order_controller.py  ← uses only the public API
from users.user_service import UserService  # public API only
from orders.order_service import OrderService

class OrderController:
    def place_order(self):
        data    = request.json
        address = UserService().get_shipping_address(data["user_id"])
        return jsonify(OrderService().place(data, address))

# UserRepository can be refactored freely — OrderController never touches it.
// users/UserService.ts  ← public API of the users/ feature
export interface ShippingAddress { street: string; city: string; country: string; }

export class UserService {
  async getShippingAddress(userId: string): Promise {
    const repo = new UserRepository(); // internal
    const user = await repo.findById(userId);
    return { street: user.street, city: user.city, country: user.country };
  }
}

// users/index.ts  ← barrel: only exports public API
export { UserService } from "./UserService";
export type { ShippingAddress } from "./UserService";
// UserRepository is NOT exported

// orders/OrderController.ts  ← uses only the barrel (public API)
import { UserService } from "../users"; // barrel import only
import { OrderService } from "./OrderService";

export class OrderController {
  async placeOrder(req: { body: { userId: string } }): Promise {
    const address = await new UserService().getShippingAddress(req.body.userId);
    return new OrderService().place(req.body, address);
  }
}

// UserRepository can be refactored freely — OrderController never touches it.

💡Conclusión clave

Package by Feature mejora la detectabilidad pero no aplica fronteras por sí solo. Necesitas modificadores de acceso o archivos barrel explícitos para evitar que las funcionalidades alcancen los internos de las demás. La estructura te dice dónde están las cosas; el control de acceso te dice qué puede llamar a qué.

🔧 Algunos ejercicios pueden tener errores. Si algo parece incorrecto, usa el botón Feedback (abajo a la derecha) para reportarlo — nos ayuda a corregirlo rápido.

Pista: Paquete por Funcionalidad mejora la descubribilidad pero no impone límites. Necesitas modificadores de acceso o puertos explícitos para evitar que las funcionalidades accedan a las partes internas de otras.

✗ Tu versión