Arquitectura & DevOps
Patrones de diseño, pipelines y mejores prácticas de la industria
Selecciona una arquitectura para ver detalles
Progresión estructurada desde fundamentos hasta arquitecturas enterprise
Arquitectura moderna de flujo de datos para procesamiento en tiempo real y por lotes
# etl_dag.py - Pipeline de datos completo
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.postgres.operators.postgres import PostgresOperator
from datetime import datetime, timedelta
# Configuración del DAG
default_args = {
'owner': 'data-team',
'depends_on_past': False,
'start_date': datetime(2025, 1, 1),
'retries': 3,
'retry_delay': timedelta(minutes=5),
}
with DAG(
'etl_pipeline_prod',
default_args=default_args,
schedule_interval='0 6 * * *', # Diario a las 6 AM
catchup=False,
tags=['etl', 'production']
) as dag:
# Task 1: Extraer datos desde API
extract = PythonOperator(
task_id='extract_from_api',
python_callable=extract_data
)
# Task 2: Transformar con Spark
transform = PythonOperator(
task_id='transform_spark',
python_callable=spark_transform
)
# Task 3: Cargar a Data Warehouse
load = PostgresOperator(
task_id='load_to_warehouse',
postgres_conn_id='warehouse_conn',
sql='sql/load_facts.sql'
)
# Definir dependencias
extract >> transform >> load Vocabulario empresarial común que asegura que todos hablen el mismo idioma de datos
Políticas que definen quién puede ver, usar y modificar cada activo de datos
Mapeo de dependencias y linaje de datos desde origen hasta consumo
Adherencia a regulaciones de privacidad y estándares de la industria
Plataforma de analytics de Microsoft con integración nativa a Azure y Microsoft 365
Líder en visualización analítica con VizQL y exploración de datos intuitiva
BI moderno con LookML para definir métricas consistentes en toda la organización
Framework open-source para crear data apps interactivas con Python puro
El estándar de la industria para orquestación de workflows de datos, nacido en Airbnb
Orquestador moderno centrado en assets, no en tareas. Diseñado para el ciclo de vida completo
Orquestación simple y flexible con decoradores Python. Filosofía de "ingeniería negativa"
0 */2 * * * Cada 2 horas on: file_arrival Trigger por evento @asset_updated Cuando el asset cambia trigger_dag() Ejecución manual Arquitecturas probadas para pipelines de datos escalables
Patrón de organización de datos multicapa para Data Lakehouses
Datos crudos sin transformación
Datos limpios y conformados
Datos listos para análisis
Rastrea y captura cambios de base de datos en tiempo real para sistemas downstream
Propiedad descentralizada de datos con arquitectura orientada a dominios
Los equipos son dueños de sus datos de principio a fin
Tratar datos con pensamiento de producto
Infraestructura como plataforma
Descentralizado con estándares
Integración de datos unificada usando metadatos activos e IA/ML
Modelos separados de lectura y escritura para sistemas escalables orientados a eventos
Estrategias para rastrear cambios históricos en tablas de dimensión
Nunca actualizar valor original
-- No UPDATE allowed Reemplazar con nuevo valor
UPDATE dim SET name = 'New' Mantener historial completo
INSERT + valid_from/to Almacenar anterior + actual
current_val, previous_val Metodología ágil y escalable de modelado de data warehouse
Claves de negocio (identificadores únicos)
Relaciones entre hubs
Atributos descriptivos + historial
Patrones escalables para procesamiento moderno de datos
Procesamiento paralelo batch y streaming para resultados precisos y de baja latencia
Procesa todos los datos históricos
Procesa datos recientes en tiempo real
Lambda simplificado usando solo streaming para todo el procesamiento
Batch y streaming unificado en Delta Lake con transacciones ACID
Garantías transaccionales en data lakes
Consultar versiones anteriores de datos
Mismo formato para ambos paradigmas
Validación automática de esquema
Dos enfoques fundamentales para integración y transformación de datos
-- models/staging/stg_orders.sql
{{ config(materialized='incremental') }}
SELECT
order_id,
customer_id,
order_date,
total_amount,
{{ dbt_utils.generate_surrogate_key(['order_id']) }} as order_sk
FROM {{ source('raw', 'orders') }}
{% if is_incremental() %}
WHERE order_date > (SELECT MAX(order_date) FROM {{ this }})
{% endif %} Estrategias de procesamiento en tiempo real para flujos de datos continuos
Ventanas Tumbling, Sliding, Session para agregaciones basadas en tiempo
Manejo de datos tardíos con procesamiento de tiempo de evento
Garantiza que cada evento se procese exactamente una vez
Control de flujo cuando los consumidores no pueden seguir el ritmo
Comparación de arquitecturas de datos modernas y modelado dimensional
Repositorio centralizado para almacenar datos crudos a escala
Almacenamiento estructurado optimizado para analytics y BI
Combina la flexibilidad del Data Lake con las capacidades del Data Warehouse
Formatos abiertos que habilitan funcionalidad Lakehouse en Data Lakes
Databricks
Netflix → Apache
Uber → Apache
Ocultar datos internos (private) y controlar acceso mediante métodos públicos (getters/setters).
public class Empleado {
private String rut; // Oculto
private double salario;
public String getRut() {
return this.rut;
}
public void setSalario(double s) {
if (s >= 0) this.salario = s;
}
} Crear clases hijas que heredan atributos y métodos de una clase padre. Reutilización de código.
public class Persona {
protected String nombre;
}
public class Estudiante extends Persona {
private String matricula;
public Estudiante(String n, String m) {
super(); // Llama constructor padre
this.nombre = n;
this.matricula = m;
}
} "Muchas formas": Un mismo método con diferentes comportamientos según la clase que lo implemente.
public abstract class Vehiculo {
public abstract void acelerar();
}
public class Auto extends Vehiculo {
@Override
public void acelerar() {
System.out.println("Pedal");
}
}
// Una variable padre puede referenciar hijos
Vehiculo v = new Auto();
v.acelerar(); // Ejecuta versión de Auto Definir contratos (interfaces) que especifican QUÉ hacer, no CÓMO. Desacoplar implementación.
public interface Registrable {
String getId();
boolean validar();
}
public class Producto implements Registrable {
private String codigo;
@Override
public String getId() {
return this.codigo;
}
@Override
public boolean validar() {
return codigo != null;
}
} Mismo input produce mismo output. Sin efectos secundarios.
f(x) = x * 2Los datos no se modifican despues de crearse.
const newList = [...list, item]Funciones que reciben o retornan funciones.
list.map(x => x * 2)Combinar funciones para crear nuevas funciones.
compose(f, g)(x)Una clase debe tener una sola razón para cambiar. Separar responsabilidades en clases especializadas.
class Empleado { guardarBD(); validar(); enviarEmail(); } class Empleado + EmpleadoValidator + EmpleadoRepository Abierto para extensión, cerrado para modificación. Usar herencia o interfaces para agregar comportamiento.
if (tipo == "tarjeta") {...} else if (tipo == "paypal") {...} interface MetodoPago { procesar(); } → TarjetaPago, PayPalPago Subclases deben ser sustituibles por su clase base sin romper el programa. Diseño de herencia correcto.
class Cuadrado extends Rectangulo // setWidth rompe setHeight interface Forma { getArea(); } → Cuadrado, Rectangulo Interfaces pequeñas y específicas mejor que una interfaz grande. No obligar a implementar métodos no usados.
interface Trabajador { trabajar(); comer(); dormir(); } interface Trabajable + Alimentable + Descansable Depender de abstracciones, no de concretos. Inyectar dependencias mediante interfaces.
class Servicio { private MySQLRepo repo = new MySQLRepo(); } class Servicio { Servicio(IRepository repo) {...} } Nombres que revelan intencion. Evitar abreviaciones.
int d; int elapsedDays; Funciones pequenas que hacen una sola cosa.
Menos de 20 lineas El codigo debe ser auto-documentado.
i++; // increment i Long Method, Duplicate Code, God Class.
Extract Method // TDD - Test Driven Development con JUnit 5
// Ciclo: RED → GREEN → REFACTOR
// STEP 1: RED - Escribir test que falla
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
@DisplayName("Suma de dos números positivos")
void testSumaPositivos() {
Calculator calc = new Calculator();
assertEquals(5, calc.sumar(2, 3));
}
@Test
@DisplayName("Suma con número negativo")
void testSumaConNegativo() {
Calculator calc = new Calculator();
assertEquals(-1, calc.sumar(2, -3));
}
@Test
@DisplayName("División por cero lanza excepción")
void testDivisionPorCero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class,
() -> calc.dividir(10, 0));
}
}
// STEP 2: GREEN - Implementar código mínimo
public class Calculator {
public int sumar(int a, int b) {
return a + b;
}
public int dividir(int a, int b) {
if (b == 0) {
throw new ArithmeticException("División por cero");
}
return a / b;
}
}
// STEP 3: REFACTOR - Mejorar sin romper tests
// Los tests pasan → código validado ✓ // BDD - Behavior Driven Development
// Lenguaje: Gherkin (Given-When-Then)
// login.feature
Feature: Inicio de sesión de usuario
Como usuario registrado
Quiero iniciar sesión en el sistema
Para acceder a mi cuenta
Scenario: Login exitoso con credenciales válidas
Given un usuario registrado con email "juan@email.com"
And su contraseña es "Password123"
When el usuario ingresa sus credenciales
And hace clic en "Iniciar Sesión"
Then debe ver el mensaje "Bienvenido, Juan"
And debe ser redirigido al dashboard
Scenario: Login fallido con contraseña incorrecta
Given un usuario registrado con email "juan@email.com"
When el usuario ingresa email "juan@email.com"
And ingresa contraseña "wrongpassword"
And hace clic en "Iniciar Sesión"
Then debe ver el mensaje de error "Credenciales inválidas"
And debe permanecer en la página de login
// LoginSteps.java - Implementación de pasos
import io.cucumber.java.en.*;
public class LoginSteps {
private LoginPage loginPage;
private String mensaje;
@Given("un usuario registrado con email {string}")
public void usuarioRegistrado(String email) {
// Setup: crear usuario en BD de prueba
}
@When("el usuario ingresa sus credenciales")
public void ingresaCredenciales() {
loginPage.enterCredentials(email, password);
}
@Then("debe ver el mensaje {string}")
public void verificarMensaje(String expected) {
assertEquals(expected, loginPage.getMessage());
}
} // Contract Testing con Pact
// Verifica contratos entre servicios (Consumer-Driven)
// Consumer Side (Frontend/Mobile)
const { Pact } = require('@pact-foundation/pact');
describe('User API Contract', () => {
const provider = new Pact({
consumer: 'FrontendApp',
provider: 'UserService',
port: 1234
});
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
describe('GET /api/users/:id', () => {
it('returns user data for valid ID', async () => {
// Define expected interaction
await provider.addInteraction({
state: 'user with ID 1 exists',
uponReceiving: 'a request for user 1',
withRequest: {
method: 'GET',
path: '/api/users/1',
headers: { 'Accept': 'application/json' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: 1,
name: Matchers.string('John Doe'),
email: Matchers.email()
}
}
});
// Execute consumer code
const user = await userClient.getUser(1);
expect(user.name).toBeDefined();
});
});
});
// Provider Side (Backend) - Verifica contra el contrato
// El Pact Broker almacena los contratos generados if (x > 10) if (x >= 10) // Mutation Testing con PIT (Java)
// Evalúa la calidad de los tests introduciendo mutaciones
// pom.xml - Configuración de PIT
org.pitest
pitest-maven
1.15.0
com.myapp.services.*
com.myapp.services.*Test
CONDITIONALS_BOUNDARY
INCREMENTS
MATH
NEGATE_CONDITIONALS
RETURN_VALS
// Código Original
public class PriceCalculator {
public double calculateDiscount(double price, int quantity) {
if (quantity > 10) { // Mutación: > → >=
return price * 0.9; // Mutación: 0.9 → 0.8
}
return price;
}
}
// Test Original - ¿Detecta las mutaciones?
@Test
void testDescuentoPorCantidad() {
PriceCalculator calc = new PriceCalculator();
// Este test NO detecta mutación ">" → ">="
assertEquals(90.0, calc.calculateDiscount(100.0, 15));
}
// Test Mejorado - Detecta boundary mutation
@Test
void testBoundaryCondition() {
PriceCalculator calc = new PriceCalculator();
// Exactly 10 items → NO discount
assertEquals(100.0, calc.calculateDiscount(100.0, 10));
// 11 items → WITH discount
assertEquals(90.0, calc.calculateDiscount(100.0, 11));
}
// Ejecutar: mvn org.pitest:pitest-maven:mutationCoverage
// Reporte: target/pit-reports/index.html // Integration Testing con Spring Boot
// Prueba la interacción entre componentes reales
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.context.TestPropertySource;
import org.junit.jupiter.api.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setup() {
userRepository.deleteAll();
}
@Test
@Order(1)
@DisplayName("POST /api/users - Crear usuario")
void createUser() throws Exception {
String userJson = """
{
"name": "Juan Pérez",
"email": "juan@email.com",
"role": "USER"
}
""";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(userJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.name").value("Juan Pérez"));
// Verificar que se guardó en BD real
assertTrue(userRepository.findByEmail("juan@email.com").isPresent());
}
@Test
@Order(2)
@DisplayName("GET /api/users/{id} - Obtener usuario existente")
void getUserById() throws Exception {
// Arrange: crear usuario directamente en BD
User saved = userRepository.save(new User("Test", "test@email.com"));
// Act & Assert
mockMvc.perform(get("/api/users/" + saved.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Test"));
}
} // E2E Testing con Playwright
// Prueba flujos completos desde la perspectiva del usuario
import { test, expect } from '@playwright/test';
test.describe('Flujo de Compra E-Commerce', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://myshop.com');
});
test('Usuario completa compra exitosamente', async ({ page }) => {
// 1. Buscar producto
await page.fill('[data-testid="search-input"]', 'laptop');
await page.click('[data-testid="search-button"]');
await expect(page.locator('.product-card')).toHaveCount.greaterThan(0);
// 2. Seleccionar producto
await page.click('.product-card:first-child');
await expect(page).toHaveURL(/\/products\/\d+/);
// 3. Agregar al carrito
await page.click('[data-testid="add-to-cart"]');
await expect(page.locator('.cart-badge')).toHaveText('1');
// 4. Ir al checkout
await page.click('[data-testid="cart-icon"]');
await page.click('[data-testid="checkout-button"]');
// 5. Completar datos de envío
await page.fill('#shipping-name', 'Juan Pérez');
await page.fill('#shipping-address', 'Av. Principal 123');
await page.fill('#shipping-city', 'Santiago');
await page.click('[data-testid="continue-payment"]');
// 6. Completar pago (sandbox)
await page.fill('#card-number', '4242424242424242');
await page.fill('#card-expiry', '12/25');
await page.fill('#card-cvv', '123');
await page.click('[data-testid="pay-button"]');
// 7. Verificar confirmación
await expect(page.locator('.order-confirmation')).toBeVisible();
await expect(page.locator('.order-number')).toHaveText(/ORD-\d+/);
});
test('Validación de campos obligatorios', async ({ page }) => {
await page.goto('/checkout');
await page.click('[data-testid="continue-payment"]');
await expect(page.locator('.error-message')).toContainText('Nombre requerido');
});
}); Aislar dependencias con Mocks
// TDD - Test Driven Development con JUnit 5
// Ciclo: RED → GREEN → REFACTOR
// STEP 1: RED - Escribir test que falla
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
@DisplayName("Suma de dos números positivos")
void testSumaPositivos() {
Calculator calc = new Calculator();
assertEquals(5, calc.sumar(2, 3));
}
@Test
@DisplayName("Suma con número negativo")
void testSumaConNegativo() {
Calculator calc = new Calculator();
assertEquals(-1, calc.sumar(2, -3));
}
@Test
@DisplayName("División por cero lanza excepción")
void testDivisionPorCero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class,
() -> calc.dividir(10, 0));
}
}
// STEP 2: GREEN - Implementar código mínimo
public class Calculator {
public int sumar(int a, int b) {
return a + b;
}
public int dividir(int a, int b) {
if (b == 0) {
throw new ArithmeticException("División por cero");
}
return a / b;
}
}
// STEP 3: REFACTOR - Mejorar sin romper tests
// Los tests pasan → código validado ✓ // Singleton Pattern - Thread-Safe (Double-Checked Locking)
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private Connection connection;
private DatabaseConnection() {
// Private constructor prevents direct instantiation
this.connection = createConnection();
}
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public void executeQuery(String sql) {
// Execute SQL using the single connection
}
}
// Uso:
DatabaseConnection db = DatabaseConnection.getInstance();
db.executeQuery("SELECT * FROM users"); // Factory Method Pattern
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
public class SMSNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Push: " + message);
}
}
// Factory Creator
public abstract class NotificationFactory {
public abstract Notification createNotification();
public void notifyUser(String message) {
Notification notification = createNotification();
notification.send(message);
}
}
public class EmailFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new EmailNotification();
}
}
// Uso:
NotificationFactory factory = new EmailFactory();
factory.notifyUser("Tu pedido ha sido enviado!"); // Builder Pattern - Fluent Interface
public class User {
private final String firstName; // Requerido
private final String lastName; // Requerido
private final String email; // Opcional
private final int age; // Opcional
private final String phone; // Opcional
private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
}
public static class UserBuilder {
private final String firstName; // Requerido
private final String lastName; // Requerido
private String email = "";
private int age = 0;
private String phone = "";
public UserBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public UserBuilder email(String email) {
this.email = email;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public User build() {
return new User(this);
}
}
}
// Uso con fluent interface:
User user = new User.UserBuilder("Juan", "Pérez")
.email("juan@example.com")
.age(28)
.phone("+56912345678")
.build(); // Adapter Pattern - Integración de APIs externas
// Interfaz que espera nuestro sistema
public interface PaymentProcessor {
void processPayment(double amount, String currency);
boolean refund(String transactionId);
}
// API externa con interfaz diferente
public class StripeAPI {
public String charge(int amountInCents, String cur) {
// Lógica de Stripe...
return "txn_" + System.currentTimeMillis();
}
public boolean cancelCharge(String chargeId) {
// Lógica de cancelación...
return true;
}
}
// Adapter: adapta Stripe a nuestra interfaz
public class StripePaymentAdapter implements PaymentProcessor {
private final StripeAPI stripeAPI;
public StripePaymentAdapter(StripeAPI stripeAPI) {
this.stripeAPI = stripeAPI;
}
@Override
public void processPayment(double amount, String currency) {
// Convertimos el monto a centavos
int amountInCents = (int) (amount * 100);
stripeAPI.charge(amountInCents, currency);
}
@Override
public boolean refund(String transactionId) {
return stripeAPI.cancelCharge(transactionId);
}
}
// Uso: nuestro código usa la interfaz estándar
PaymentProcessor processor = new StripePaymentAdapter(new StripeAPI());
processor.processPayment(99.99, "USD"); // Decorator Pattern - Añadir funcionalidad dinámicamente
public interface Coffee {
String getDescription();
double getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Café simple";
}
@Override
public double getCost() {
return 1.00;
}
}
// Decorador base
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
}
// Decoradores concretos
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", con leche";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.50;
}
}
public class VanillaDecorator extends CoffeeDecorator {
public VanillaDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", con vainilla";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.70;
}
}
// Uso: composición dinámica
Coffee miCafe = new VanillaDecorator(
new MilkDecorator(
new SimpleCoffee()
)
);
System.out.println(miCafe.getDescription()); // Café simple, con leche, con vainilla
System.out.println("$" + miCafe.getCost()); // $2.20 // Observer Pattern - Sistema de eventos
import java.util.ArrayList;
import java.util.List;
// Subject (Publisher)
public interface StockSubject {
void attach(StockObserver observer);
void detach(StockObserver observer);
void notifyObservers();
}
// Observer (Subscriber)
public interface StockObserver {
void update(String stockSymbol, double price);
}
// Concrete Subject
public class Stock implements StockSubject {
private List observers = new ArrayList<>();
private String symbol;
private double price;
public Stock(String symbol, double price) {
this.symbol = symbol;
this.price = price;
}
public void setPrice(double price) {
this.price = price;
notifyObservers(); // Notifica cambios automáticamente
}
@Override
public void attach(StockObserver observer) {
observers.add(observer);
}
@Override
public void detach(StockObserver observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (StockObserver observer : observers) {
observer.update(symbol, price);
}
}
}
// Concrete Observer
public class StockAlert implements StockObserver {
private String name;
public StockAlert(String name) {
this.name = name;
}
@Override
public void update(String stockSymbol, double price) {
System.out.println(name + ": " + stockSymbol + " ahora vale $" + price);
}
}
// Uso:
Stock apple = new Stock("AAPL", 150.0);
apple.attach(new StockAlert("Inversor1"));
apple.attach(new StockAlert("Inversor2"));
apple.setPrice(155.0); // Notifica a todos los observers // Strategy Pattern - Algoritmos intercambiables
public interface PaymentStrategy {
void pay(double amount);
boolean validate();
}
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String cvv;
public CreditCardPayment(String cardNumber, String cvv) {
this.cardNumber = cardNumber;
this.cvv = cvv;
}
@Override
public boolean validate() {
return cardNumber.length() == 16 && cvv.length() == 3;
}
@Override
public void pay(double amount) {
System.out.println("Pagando $" + amount + " con tarjeta: ****" +
cardNumber.substring(12));
}
}
public class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public boolean validate() {
return email.contains("@");
}
@Override
public void pay(double amount) {
System.out.println("Pagando $" + amount + " via PayPal: " + email);
}
}
// Context: usa la estrategia sin conocer los detalles
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(double amount) {
if (paymentStrategy.validate()) {
paymentStrategy.pay(amount);
} else {
throw new RuntimeException("Pago inválido");
}
}
}
// Uso: el cliente elige la estrategia
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234567890123456", "123"));
cart.checkout(99.99);
// Cambiar estrategia en runtime
cart.setPaymentStrategy(new PayPalPayment("usuario@email.com"));
cart.checkout(49.99); Patrones de diseño, pipelines y mejores prácticas de la industria
Selecciona una arquitectura para ver detalles
// Entidad del dominio - sin dependencias externas
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderItem> items;
private OrderStatus status;
public Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.items = new ArrayList<>();
this.status = OrderStatus.CREATED;
}
// Business logic dentro de la entidad
public Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
public void confirm() {
if (items.isEmpty()) {
throw new OrderException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
}
} // Use Case - orquesta la lógica de negocio
public class CreateOrderUseCase {
private final OrderRepository orderRepository; // Port
private final CustomerRepository customerRepository; // Port
private final NotificationService notificationService; // Port
public CreateOrderUseCase(
OrderRepository orderRepository,
CustomerRepository customerRepository,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.customerRepository = customerRepository;
this.notificationService = notificationService;
}
public OrderOutput execute(CreateOrderInput input) {
// 1. Validar cliente existe
Customer customer = customerRepository
.findById(input.getCustomerId())
.orElseThrow(() -> new CustomerNotFoundException());
// 2. Crear entidad Order
Order order = new Order(
OrderId.generate(),
customer.getId()
);
// 3. Agregar items
input.getItems().forEach(item ->
order.addItem(item.getProductId(), item.getQuantity())
);
// 4. Persistir
orderRepository.save(order);
// 5. Notificar
notificationService.sendOrderConfirmation(order);
return OrderOutput.from(order);
}
} // Adapter - implementa el Port (Repository)
@Repository
public class JpaOrderRepository implements OrderRepository {
private final JpaOrderJpaRepository jpaRepository;
private final OrderMapper mapper;
public JpaOrderRepository(
JpaOrderJpaRepository jpaRepository,
OrderMapper mapper) {
this.jpaRepository = jpaRepository;
this.mapper = mapper;
}
@Override
public void save(Order order) {
OrderJpaEntity entity = mapper.toJpaEntity(order);
jpaRepository.save(entity);
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepository.findById(id.getValue())
.map(mapper::toDomain);
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return jpaRepository
.findByCustomerId(customerId.getValue())
.stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}
} // Port: Interface que define el contrato
// El dominio NO SABE cómo se implementa
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> findByStatus(OrderStatus status);
void delete(OrderId id);
}
// Driving Port (Input) - lo que la app expone
public interface OrderService {
OrderDto createOrder(CreateOrderCommand command);
OrderDto getOrder(UUID orderId);
void confirmOrder(UUID orderId);
void cancelOrder(UUID orderId);
} // Driving Adapter: Traduce HTTP a llamadas al Port
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService; // Driving Port
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderDto> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
// Traducir Request → Command
CreateOrderCommand command = new CreateOrderCommand(
request.getCustomerId(),
request.getItems()
);
// Llamar al Port
OrderDto order = orderService.createOrder(command);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(order);
}
@GetMapping("/{id}")
public ResponseEntity<OrderDto> getOrder(@PathVariable UUID id) {
return ResponseEntity.ok(orderService.getOrder(id));
}
} // Driven Adapter: Implementa el Port para PostgreSQL
@Repository
public class PostgresOrderRepository implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderMapper mapper;
public PostgresOrderRepository(
OrderJpaRepository jpaRepository,
OrderMapper mapper) {
this.jpaRepository = jpaRepository;
this.mapper = mapper;
}
@Override
public void save(Order order) {
// Domain → JPA Entity
OrderEntity entity = mapper.toEntity(order);
jpaRepository.save(entity);
}
@Override
public Optional<Order> findById(OrderId id) {
// JPA Entity → Domain
return jpaRepository.findById(id.getValue())
.map(mapper::toDomain);
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return jpaRepository
.findByCustomerId(customerId.getValue())
.stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}
} // Aggregate Root - Order (Entity)
public class Order extends AggregateRoot<OrderId> {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderLine> orderLines;
private OrderStatus status;
private Money totalAmount;
// Factory method - solo forma de crear
public static Order create(CustomerId customerId) {
Order order = new Order(
OrderId.generate(),
customerId,
new ArrayList<>(),
OrderStatus.DRAFT,
Money.ZERO
);
// Emitir Domain Event
order.registerEvent(new OrderCreatedEvent(order.id));
return order;
}
// Business Logic - protege invariantes
public void addLine(ProductId productId, int quantity, Money price) {
if (status != OrderStatus.DRAFT) {
throw new OrderNotEditableException(id);
}
OrderLine line = new OrderLine(productId, quantity, price);
orderLines.add(line);
recalculateTotal();
registerEvent(new OrderLineAddedEvent(id, line));
}
public void submit() {
if (orderLines.isEmpty()) {
throw new EmptyOrderException(id);
}
this.status = OrderStatus.SUBMITTED;
registerEvent(new OrderSubmittedEvent(id, totalAmount));
}
private void recalculateTotal() {
this.totalAmount = orderLines.stream()
.map(OrderLine::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
} // Value Object - Inmutable, sin identidad
public final class Money {
public static final Money ZERO = new Money(BigDecimal.ZERO, "USD");
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidMoneyException("Amount cannot be negative");
}
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = currency;
}
// Operaciones que retornan nuevas instancias
public Money add(Money other) {
validateSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}
public Money multiply(int quantity) {
return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
}
// Value Objects se comparan por valor
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.equals(money.amount) && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
} // Domain Service - lógica que no pertenece a una entidad
public class PricingService {
private final DiscountPolicyRepository discountRepo;
private final TaxCalculator taxCalculator;
public PricingService(DiscountPolicyRepository discountRepo,
TaxCalculator taxCalculator) {
this.discountRepo = discountRepo;
this.taxCalculator = taxCalculator;
}
// Operación que involucra múltiples agregados
public Money calculateFinalPrice(Order order, Customer customer) {
Money subtotal = order.getTotalAmount();
// Aplicar descuentos según políticas
DiscountPolicy policy = discountRepo.findForCustomer(customer);
Money afterDiscount = policy.apply(subtotal, customer);
// Calcular impuestos según ubicación
Money taxes = taxCalculator.calculate(afterDiscount, customer.getAddress());
return afterDiscount.add(taxes);
}
}
// Domain Event - algo que ocurrió en el dominio
public record OrderSubmittedEvent(
OrderId orderId,
Money totalAmount,
Instant occurredAt
) implements DomainEvent {
public OrderSubmittedEvent(OrderId orderId, Money totalAmount) {
this(orderId, totalAmount, Instant.now());
}
} // Command - intención de cambiar estado
public record CreateOrderCommand(
UUID customerId,
List<OrderLineDto> items
) implements Command {}
// Command Handler - ejecuta el comando
@Component
public class CreateOrderCommandHandler implements CommandHandler<CreateOrderCommand> {
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
private final EventPublisher eventPublisher;
@Override
@Transactional
public void handle(CreateOrderCommand command) {
// 1. Validar que cliente existe
Customer customer = customerRepository.findById(command.customerId())
.orElseThrow(() -> new CustomerNotFoundException(command.customerId()));
// 2. Crear agregado Order
Order order = Order.create(customer.getId());
// 3. Agregar líneas
command.items().forEach(item ->
order.addLine(item.productId(), item.quantity(), item.price())
);
// 4. Persistir en Write DB
orderRepository.save(order);
// 5. Publicar eventos para actualizar Read DB
order.getDomainEvents().forEach(eventPublisher::publish);
}
} // Query - solicitud de datos (no modifica estado)
public record GetOrderDetailsQuery(UUID orderId) implements Query {}
// Query Handler - ejecuta la consulta
@Component
public class GetOrderDetailsQueryHandler
implements QueryHandler<GetOrderDetailsQuery, OrderDetailsDto> {
private final OrderReadRepository readRepository; // Read-optimized DB
@Override
public OrderDetailsDto handle(GetOrderDetailsQuery query) {
// Lee de la base optimizada para lecturas
return readRepository.findById(query.orderId())
.orElseThrow(() -> new OrderNotFoundException(query.orderId()));
}
}
// Read Model - optimizado para consultas
@Document(collection = "order_details")
public class OrderDetailsReadModel {
@Id
private String orderId;
private String customerName;
private String customerEmail;
private List<OrderLineView> items;
private String status;
private BigDecimal totalAmount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Datos denormalizados para evitar JOINs
} // Projection - actualiza Read Model cuando ocurren eventos
@Component
public class OrderProjection {
private final OrderReadRepository readRepository;
@EventHandler
public void on(OrderCreatedEvent event) {
OrderDetailsReadModel model = new OrderDetailsReadModel();
model.setOrderId(event.orderId().toString());
model.setStatus("DRAFT");
model.setCreatedAt(LocalDateTime.now());
model.setItems(new ArrayList<>());
readRepository.save(model);
}
@EventHandler
public void on(OrderLineAddedEvent event) {
OrderDetailsReadModel model = readRepository.findById(event.orderId().toString())
.orElseThrow();
model.getItems().add(new OrderLineView(
event.productId(),
event.productName(), // Denormalizado
event.quantity(),
event.price()
));
model.setUpdatedAt(LocalDateTime.now());
readRepository.save(model);
}
@EventHandler
public void on(OrderSubmittedEvent event) {
OrderDetailsReadModel model = readRepository.findById(event.orderId().toString())
.orElseThrow();
model.setStatus("SUBMITTED");
model.setTotalAmount(event.totalAmount());
readRepository.save(model);
}
} // Event Sourced Aggregate
public class Order extends EventSourcedAggregate {
private OrderId id;
private CustomerId customerId;
private List<OrderLine> orderLines = new ArrayList<>();
private OrderStatus status;
private Money totalAmount = Money.ZERO;
// Constructor desde eventos
public Order(List<DomainEvent> events) {
events.forEach(this::apply);
}
// Comandos producen eventos (no modifican estado directamente)
public void createOrder(CustomerId customerId) {
// Validaciones
if (customerId == null) {
throw new InvalidCustomerException();
}
// Emitir evento
raise(new OrderCreatedEvent(OrderId.generate(), customerId, Instant.now()));
}
public void addLine(ProductId productId, int quantity, Money price) {
if (status != OrderStatus.DRAFT) {
throw new OrderNotEditableException(id);
}
raise(new OrderLineAddedEvent(id, productId, quantity, price, Instant.now()));
}
// Handlers aplican eventos al estado
@EventHandler
private void on(OrderCreatedEvent event) {
this.id = event.orderId();
this.customerId = event.customerId();
this.status = OrderStatus.DRAFT;
}
@EventHandler
private void on(OrderLineAddedEvent event) {
OrderLine line = new OrderLine(event.productId(), event.quantity(), event.price());
this.orderLines.add(line);
this.totalAmount = totalAmount.add(line.getSubtotal());
}
} // Event Store - almacena eventos inmutables
public interface EventStore {
void save(AggregateId id, List<DomainEvent> events, long expectedVersion);
List<DomainEvent> getEvents(AggregateId id);
List<DomainEvent> getEvents(AggregateId id, long fromVersion);
}
@Repository
public class PostgresEventStore implements EventStore {
private final JdbcTemplate jdbc;
private final ObjectMapper mapper;
@Override
@Transactional
public void save(AggregateId id, List<DomainEvent> events, long expectedVersion) {
// Optimistic locking
long currentVersion = getCurrentVersion(id);
if (currentVersion != expectedVersion) {
throw new ConcurrencyException(id, expectedVersion, currentVersion);
}
long version = expectedVersion;
for (DomainEvent event : events) {
version++;
jdbc.update(
"INSERT INTO events (aggregate_id, version, type, data, timestamp) VALUES (?, ?, ?, ?, ?)",
id.toString(),
version,
event.getClass().getName(),
mapper.writeValueAsString(event),
event.occurredAt()
);
}
}
@Override
public List<DomainEvent> getEvents(AggregateId id) {
return jdbc.query(
"SELECT * FROM events WHERE aggregate_id = ? ORDER BY version",
this::mapToEvent,
id.toString()
);
}
} // Snapshots - optimización para agregados con muchos eventos
public interface SnapshotStore {
Optional<Snapshot> getLatest(AggregateId id);
void save(Snapshot snapshot);
}
public record Snapshot(
AggregateId aggregateId,
long version,
String state,
Instant createdAt
) {}
// Repository que usa Event Sourcing + Snapshots
public class EventSourcedOrderRepository implements OrderRepository {
private final EventStore eventStore;
private final SnapshotStore snapshotStore;
private static final int SNAPSHOT_FREQUENCY = 100;
@Override
public Optional<Order> findById(OrderId id) {
// 1. Buscar snapshot más reciente
Optional<Snapshot> snapshot = snapshotStore.getLatest(id);
// 2. Cargar eventos desde snapshot o desde el inicio
List<DomainEvent> events = snapshot
.map(s -> eventStore.getEvents(id, s.version()))
.orElse(eventStore.getEvents(id));
if (events.isEmpty() && snapshot.isEmpty()) {
return Optional.empty();
}
// 3. Reconstruir agregado
Order order = snapshot
.map(s -> deserialize(s.state()))
.orElse(new Order());
events.forEach(order::apply);
return Optional.of(order);
}
@Override
public void save(Order order) {
List<DomainEvent> newEvents = order.getUncommittedEvents();
eventStore.save(order.getId(), newEvents, order.getVersion());
// Crear snapshot cada N eventos
if (order.getVersion() % SNAPSHOT_FREQUENCY == 0) {
snapshotStore.save(new Snapshot(
order.getId(),
order.getVersion(),
serialize(order),
Instant.now()
));
}
}
} Metodología iterativa e incremental para equipos de alto rendimiento
Sprint Goal Definition
Stand-up Meeting
Demo to Stakeholders
Continuous Improvement