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:
- Crear una reserva.
- Listar reservas activas.
- 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.