Transaction Script: Patrón simple para lógica de negocio (Catalog of Patterns of EAA — Martin Fowler)

Transaction Script: Un patrón sencillo para organizar la lógica empresarial

Cuando se desarrollan aplicaciones empresariales, uno de los mayores retos es cómo organizar la lógica de negocio. Existen muchas formas de hacerlo, y Martin Fowler, en su famoso libro Patterns of Enterprise Application Architecture (2003), propuso un catálogo de patrones para enfrentar este tipo de problemas.

En este artículo exploraremos el patrón Transaction Script, uno de los más simples y directos del catálogo, acompañado de un ejemplo práctico en Python.

¿Qué es Transaction Script?

El patrón Transaction Script organiza la lógica de negocio en procedimientos individuales, donde cada procedimiento maneja una única transacción o petición del sistema.

En otras palabras:

  • Si un cliente quiere crear una reserva → existe un script para eso.
  • Si un cliente quiere cancelar una reserva → existe otro script dedicado.

Cada script contiene:

  • Validaciones de entrada.
  • Reglas de negocio.
  • Operaciones de lectura/escritura en base de datos.
  • La confirmación de la transacción.

¿Cuándo usarlo?

Transaction Script es ideal cuando:

  • La aplicación es relativamente pequeña.
  • Las reglas de negocio son simples y fáciles de describir paso a paso.
  • Se busca rapidez en el desarrollo.
  • No se justifica todavía invertir en arquitecturas complejas como Domain Model.

Ejemplos de uso típico:

  • Prototipos rápidos.
  • Aplicaciones CRUD sencillas.
  • Servicios que procesan peticiones claras y lineales.

¿Cuándo evitarlo?

No es recomendable si:

  • El dominio es complejo y la lógica empieza a repetirse en varios scripts.
  • Hay muchas reglas compartidas entre operaciones.
  • Se necesita reutilización de lógica o comportamiento rico en objetos.

En estos casos, es mejor migrar a patrones como Domain Model o Service Layer.

Ejemplo práctico: Sistema simple de reservas

Supongamos que estamos creando una pequeña API que permite:

  1. Crear una reserva.
  2. Listar reservas activas.
  3. Cancelar una reserva.

Usaremos Python + Flask + SQLite para mostrar el patrón en acción.

1. Crear la base de datos

Archivo db_init.py:

import sqlite3

def init_db(path='reservas.db'):
    conn = sqlite3.connect(path)
    c = conn.cursor()
    c.execute('''
    CREATE TABLE IF NOT EXISTS reservations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        customer_name TEXT NOT NULL,
        resource TEXT NOT NULL,
        start_time TEXT NOT NULL,
        end_time TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'active',
        created_at TEXT NOT NULL DEFAULT (datetime('now'))
    );
    ''')
    conn.commit()
    conn.close()

if __name__ == '__main__':
    init_db()
    print("Base de datos inicializada en reservas.db")

2. Transaction Scripts

Archivo transactions.py:

import sqlite3
from contextlib import contextmanager

DB_PATH = 'reservas.db'

@contextmanager
def get_conn():
    conn = sqlite3.connect(DB_PATH)
    try:
        yield conn
        conn.commit()
    except:
        conn.rollback()
        raise
    finally:
        conn.close()

def select_active_reservations():
    with get_conn() as conn:
        c = conn.cursor()
        c.execute("SELECT id, customer_name, resource, start_time, end_time, status, created_at FROM reservations WHERE status = 'active'")
        return c.fetchall()

def create_reservation_tx(customer_name, resource, start_time, end_time):
    if start_time >= end_time:
        raise ValueError("start_time debe ser anterior a end_time")

    with get_conn() as conn:
        c = conn.cursor()
        c.execute('''
            SELECT COUNT(*) FROM reservations
            WHERE resource = ? AND status = 'active'
            AND NOT (end_time <= ? OR start_time >= ?)
        ''', (resource, start_time, end_time))
        (overlap_count,) = c.fetchone()
        if overlap_count > 0:
            raise ValueError("Ya existe una reserva que se solapa en ese horario")

        c.execute('''
            INSERT INTO reservations (customer_name, resource, start_time, end_time)
            VALUES (?, ?, ?, ?)
        ''', (customer_name, resource, start_time, end_time))
        return c.lastrowid

def cancel_reservation_tx(reservation_id):
    with get_conn() as conn:
        c = conn.cursor()
        c.execute('SELECT status FROM reservations WHERE id = ?', (reservation_id,))
        row = c.fetchone()
        if not row:
            raise ValueError("Reserva no encontrada")
        if row[0] != 'active':
            raise ValueError("La reserva ya no está activa")
        c.execute('UPDATE reservations SET status = ? WHERE id = ?', ('cancelled', reservation_id))
        return True

3. API con Flask

Archivo app.py:

from flask import Flask, request, jsonify
from transactions import create_reservation_tx, cancel_reservation_tx, select_active_reservations
from db_init import init_db

app = Flask(__name__)
init_db()

@app.route('/reservations', methods=['POST'])
def create_reservation():
    payload = request.get_json()
    try:
        res_id = create_reservation_tx(
            customer_name=payload['customer_name'],
            resource=payload['resource'],
            start_time=payload['start_time'],
            end_time=payload['end_time']
        )
        return jsonify({"id": res_id}), 201
    except Exception as e:
        return jsonify({"error": str(e)}), 400

@app.route('/reservations', methods=['GET'])
def list_reservations():
    rows = select_active_reservations()
    reservations = [
        {
            "id": r[0],
            "customer_name": r[1],
            "resource": r[2],
            "start_time": r[3],
            "end_time": r[4],
            "status": r[5],
            "created_at": r[6]
        }
        for r in rows
    ]
    return jsonify(reservations), 200

@app.route('/reservations/<int:res_id>/cancel', methods=['POST'])
def cancel_reservation(res_id):
    try:
        cancel_reservation_tx(res_id)
        return jsonify({"status": "cancelled"}), 200
    except Exception as e:
        return jsonify({"error": str(e)}), 400

if __name__ == '__main__':
    app.run(debug=True)

4. Prueba rápida con curl

Crear una reserva:

curl -X POST http://127.0.0.1:5000/reservations 
  -H "Content-Type: application/json" 
  -d '{"customer_name":"Ana Perez","resource":"sala-A","start_time":"2025-09-20T10:00","end_time":"2025-09-20T11:00"}'

Listar reservas:

curl http://127.0.0.1:5000/reservations

Cancelar:

curl -X POST http://127.0.0.1:5000/reservations/1/cancel

Ventajas

  • Simple y directo.
  • Fácil de leer por cualquier desarrollador.
  • Ideal para prototipos o proyectos pequeños.

Desventajas

  • Tiende a duplicar lógica si crece demasiado.
  • No escala bien en dominios complejos.
  • Poco reutilizable a medida que aumentan las reglas.

Conclusión
El patrón Transaction Script es una excelente puerta de entrada al diseño de aplicaciones empresariales. Si tu sistema es pequeño, este patrón ofrece claridad y rapidez. Sin embargo, si tu aplicación crece en complejidad, tarde o temprano necesitarás evolucionar a otros patrones como Domain Model o Service Layer.

Leave a Reply