Desarrollo Full Stack para sistemas bancarios

Desarrollo Full Stack para sistemas bancarios

Construir software bancario requiere conectar conceptos de programación con reglas de negocio y una interfaz que consuma servicios de forma segura. Esta guía reúne fundamentos de Java, transacciones financieras, JavaScript y componentes web para entender cómo se relacionan esas piezas.

Los ejemplos son didácticos. No representan una implementación lista para producción ni describen APIs internas de una organización.

1. Fundamentos de Java

Java ofrece herramientas para modelar entidades, separar responsabilidades y controlar el acceso al estado de los objetos.

Clases, objetos y encapsulamiento

Una clase define atributos y métodos. Un objeto es una instancia concreta de esa clase. En un dominio bancario, estas estructuras pueden representar cuentas, clientes, tarjetas o movimientos.

El encapsulamiento protege el estado interno. En vez de permitir cambios directos sobre un saldo, una clase puede ofrecer operaciones que validen los datos antes de modificarlo.

class Cuenta {
    private double saldo;

    public double getSaldo() {
        return saldo;
    }

    public void depositar(double importe) {
        if (importe <= 0) {
            throw new IllegalArgumentException("Importe inválido");
        }
        saldo += importe;
    }
}

Este ejemplo ilustra el control de acceso, pero no es apropiado para cálculos monetarios exactos. Para dinero suele preferirse BigDecimal, evitando los errores de representación binaria de double.

Herencia, polimorfismo e interfaces

La herencia permite que una clase derive de otra y reutilice miembros accesibles. Puede expresar características comunes y especializaciones, como distintos tipos de cuenta.

El polimorfismo permite trabajar con implementaciones distintas a través de un tipo compartido. Una interfaz declara ese contrato y evita que la lógica dependa de una clase concreta.

interface MedioPago {
    void pagar(double importe);
}

class PagoTarjeta implements MedioPago {
    public void pagar(double importe) {
        System.out.println("Pago con tarjeta: " + importe);
    }
}

class PagoTransferencia implements MedioPago {
    public void pagar(double importe) {
        System.out.println("Transferencia: " + importe);
    }
}

Con esta separación se pueden incorporar implementaciones diferentes sin que todo el código dependa de una sola forma de pago.

Excepciones

Las excepciones representan situaciones que interrumpen el flujo normal. try y catch permiten gestionarlas; throws permite propagarlas. Usar excepciones específicas ayuda a distinguir problemas de negocio de fallos técnicos.

try {
    double saldo = 100.0;
    double importe = 150.0;

    if (importe > saldo) {
        throw new IllegalStateException("Saldo insuficiente");
    }
} catch (IllegalStateException error) {
    System.out.println(error.getMessage());
}

En una aplicación real, los errores deben gestionarse sin ocultar fallos ni exponer datos sensibles.

Colecciones, streams y lambdas

Las colecciones organizan grupos de objetos. List conserva una secuencia, Set evita duplicados y Map relaciona claves con valores. La elección depende de las operaciones que necesite el sistema.

Una lambda expresa una función que se puede pasar como argumento. Stream procesa secuencias con operaciones como filter, map y reduce.

import java.math.BigDecimal;
import java.util.List;

List<BigDecimal> importes = List.of(
    new BigDecimal("20.00"),
    new BigDecimal("80.00"),
    new BigDecimal("150.00")
);

List<BigDecimal> mayores = importes.stream()
    .filter(importe -> importe.compareTo(new BigDecimal("50.00")) > 0)
    .toList();

Genéricos y concurrencia

Los genéricos parametrizan clases, interfaces y métodos con tipos. Así, el compilador puede detectar varios errores antes de ejecutar el programa y se reducen las conversiones manuales.

class Resultado<T> {
    private final T dato;

    Resultado(T dato) {
        this.dato = dato;
    }

    public T getDato() {
        return dato;
    }
}

Resultado<String> resultado = new Resultado<>("OPERACIÓN ACEPTADA");

La concurrencia permite que varias tareas progresen en periodos coincidentes. En un sistema bancario, varias solicitudes podrían consultar o modificar la misma cuenta. Sin coordinación pueden aparecer condiciones de carrera o actualizaciones perdidas.

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger solicitudes = new AtomicInteger(0);
solicitudes.incrementAndGet();
System.out.println(solicitudes.get());

AtomicInteger protege esta operación concreta; no reemplaza la coordinación transaccional de una cuenta en la base de datos.

2. Cuentas, movimientos y transacciones

Una cuenta bancaria suele relacionar un identificador, un titular, una moneda, un estado y sus movimientos. El saldo disponible puede diferir del saldo contable cuando existen retenciones u operaciones pendientes.

Un movimiento registra una operación como un depósito, retiro, pago o transferencia. Para procesarlo se deben validar el importe, la moneda, los permisos, los límites y el estado de las cuentas. También es importante conservar una referencia y un resultado verificable.

import java.math.BigDecimal;

class Movimiento {
    private final String referencia;
    private final String tipo;
    private final BigDecimal importe;

    Movimiento(String referencia, String tipo, BigDecimal importe) {
        this.referencia = referencia;
        this.tipo = tipo;
        this.importe = importe;
    }
}

3. Propiedades ACID

ACID describe cuatro propiedades de las transacciones de bases de datos:

  • Atomicidad: una operación compuesta se completa entera o no se aplica. Una transferencia, por ejemplo, debe debitar una cuenta y acreditar la otra; no debe quedar aplicada a medias.
  • Consistencia: cada transacción conserva las reglas de integridad, como las monedas permitidas, las referencias únicas y los invariantes del sistema.
  • Aislamiento: define cómo interactúan las transacciones simultáneas y qué lecturas o actualizaciones pueden observar.
  • Durabilidad: los cambios confirmados deben conservarse según las garantías y la configuración de la base de datos.

En Spring, una operación de negocio puede delimitarse con @Transactional:

import org.springframework.transaction.annotation.Transactional;

@Transactional
public void registrarMovimiento() {
    // Cargar y validar las entidades.
    // Actualizar los datos relacionados.
    // Registrar el movimiento en la misma transacción.
}

Un esquema SQL simplificado permite visualizar los pasos de una transferencia:

BEGIN;

UPDATE cuenta
SET saldo = saldo - 100.00
WHERE id = 'A' AND saldo >= 100.00;

UPDATE cuenta
SET saldo = saldo + 100.00
WHERE id = 'B';

INSERT INTO movimiento(referencia, importe)
VALUES ('TRX-001', 100.00);

COMMIT;

En una aplicación real se comprueba el resultado de cada operación y se ejecuta ROLLBACK si falla una validación o una actualización. Los efectos externos, como enviar una notificación, no se revierten automáticamente con el rollback de la base de datos.

La consistencia también se puede expresar mediante validaciones:

import java.math.BigDecimal;

BigDecimal saldo = new BigDecimal("250.00");
BigDecimal importe = new BigDecimal("100.00");

if (importe.signum() <= 0 || saldo.compareTo(importe) < 0) {
    throw new IllegalArgumentException("Operación no válida");
}

Los niveles de aislamiento habituales incluyen READ COMMITTED, REPEATABLE READ y SERIALIZABLE. La selección depende de la base de datos y de las garantías que necesita la operación.

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(isolation = Isolation.SERIALIZABLE)
public void procesarOperacionCritica() {
    // Validar y actualizar datos dentro de la transacción.
}

La durabilidad depende, entre otros aspectos, de la persistencia y configuración de la base de datos. Las copias de seguridad y la recuperación ante desastres complementan esa propiedad.

4. JavaScript en una interfaz bancaria

JavaScript permite construir la interfaz que presenta información y solicita operaciones a servicios.

Variables, funciones y objetos

let y const declaran variables con alcance de bloque. Los tipos incluyen string, number, boolean, bigint, symbol, undefined y object. Para representar importes monetarios, el formato y la precisión deben acordarse con el backend.

Las funciones agrupan operaciones reutilizables. Las funciones flecha usan => y capturan el this del contexto externo.

function esImporteValido(importe) {
    return Number.isFinite(importe) && importe > 0;
}

const describirOperacion = tipo => `Operación: ${tipo}`;

Los objetos agrupan propiedades y métodos; las clases permiten crear instancias con estado y comportamiento. La composición también ayuda a mantener responsabilidades acotadas.

class Cuenta {
    constructor(id, moneda) {
        this.id = id;
        this.moneda = moneda;
    }

    describir() {
        return `${this.id} (${this.moneda})`;
    }
}

Las validaciones del navegador mejoran la interacción, pero los datos críticos deben volver a comprobarse en el servidor.

Arrays, asincronía y módulos

Un array conserva una secuencia ordenada. map transforma elementos, filter conserva los que cumplen una condición y reduce acumula un resultado.

const movimientos = [
    { tipo: "DEPOSITO", importe: 200 },
    { tipo: "RETIRO", importe: 50 },
    { tipo: "DEPOSITO", importe: 80 }
];

const depositos = movimientos.filter(m => m.tipo === "DEPOSITO");
const importes = movimientos.map(m => m.importe);
const total = movimientos.reduce((suma, m) => suma + m.importe, 0);

Una Promise representa el resultado futuro de una operación asíncrona. async y await permiten expresar ese flujo con claridad. La interfaz debe contemplar estados de carga y error.

async function consultarMovimientos() {
    const respuesta = await fetch("/api/movimientos");

    if (!respuesta.ok) {
        throw new Error(`Error HTTP: ${respuesta.status}`);
    }

    return respuesta.json();
}

Los módulos dividen el código por responsabilidades mediante export e import. Esto mantiene explícitas las dependencias y facilita probar cada parte.

// archivo: validaciones.js
export function importeValido(importe) {
    return Number.isFinite(importe) && importe > 0;
}

// archivo: transferencia.js
import { importeValido } from "./validaciones.js";
console.log(importeValido(125));

DOM, APIs REST y errores

El DOM representa el documento HTML como objetos. Los eventos notifican interacciones como clics, cambios de texto o envíos de formularios.

const boton = document.querySelector("#consultar");

boton?.addEventListener("click", () => {
    console.log("Consultar saldo");
});

Una API REST expone recursos mediante rutas y métodos HTTP. fetch envía solicitudes y JSON es un formato habitual de intercambio. Antes de procesar una respuesta se debe comprobar el estado HTTP.

async function obtenerCuenta() {
    const respuesta = await fetch("/api/cuentas/CTA-1042");

    if (!respuesta.ok) {
        throw new Error("No se pudo consultar la cuenta");
    }

    return respuesta.json();
}

try/catch permite gestionar errores durante la ejecución, especialmente en operaciones asíncronas. La aplicación debe informar al usuario sin mostrar detalles internos sensibles; los registros deben tener contexto seguro para facilitar el diagnóstico.

5. Componentes web y arquitectura de la interfaz

Web Components es un conjunto de estándares para crear elementos HTML reutilizables. Lit ofrece LitElement y la etiqueta html para declarar componentes, propiedades y plantillas.

import { LitElement, html } from "lit";

class TarjetaCuenta extends LitElement {
    static properties = {
        titular: { type: String }
    };

    constructor() {
        super();
        this.titular = "Cliente";
    }

    render() {
        return html`<p>Cuenta de ${this.titular}</p>`;
    }
}

customElements.define("tarjeta-cuenta", TarjetaCuenta);

Las propiedades reactivas notifican cambios y provocan una actualización. Lit dispone de métodos de ciclo de vida, como updated, para reaccionar a cambios. Los efectos secundarios deben gestionarse con cuidado para no duplicar solicitudes.

Las plantillas Lit insertan expresiones en posiciones definidas. Aun así, hay que tratar los valores según el contexto y evitar insertar HTML no confiable.

El routing relaciona rutas con páginas o componentes; la configuración define cómo se ensamblan y arrancan las partes de una aplicación. Los detalles de CELLS dependen de la versión y del entorno.

Pub/Sub separa a quien emite eventos de quienes los reciben. RxJS modela flujos de datos con Observables y operadores. En ambos casos conviene definir nombres, datos y ciclo de vida de los eventos.

import { Subject } from "rxjs";

const cuentaSeleccionada$ = new Subject();

const suscripcion = cuentaSeleccionada$.subscribe(cuenta => {
    console.log("Cuenta seleccionada:", cuenta.id);
});

cuentaSeleccionada$.next({ id: "CTA-1042" });
suscripcion.unsubscribe();

Los controllers pueden encapsular lógica reutilizable vinculada a componentes o páginas. Los nombres ElementController y PageController pueden pertenecer a APIs concretas de ciertas versiones de CELLS; hay que verificar la documentación del proyecto antes de usar sus constructores o métodos.

Una API permite que la interfaz solicite datos y operaciones al backend. ASO puede referirse a una capa o catálogo de servicios específico de una organización, por lo que su significado debe confirmarse en el contexto del proyecto. La gestión de estado coordina lo que una vista presenta y sus transiciones, pero la autorización, las reglas financieras y la fuente de verdad deben permanecer en el backend.

export async function obtenerCuenta(id) {
    const respuesta = await fetch(`/api/cuentas/${encodeURIComponent(id)}`);

    if (!respuesta.ok) {
        throw new Error("No se pudo obtener la cuenta");
    }

    return respuesta.json();
}

6. Pruebas y entrega continua

Git registra cambios y permite trabajar con ramas. Bitbucket aloja repositorios Git y puede integrarse con revisiones y pipelines. Las pruebas unitarias verifican unidades pequeñas de comportamiento, mientras CI/CD automatiza validaciones, construcción y despliegue bajo los controles del equipo.

import { describe, it, expect } from "vitest";

function importeValido(importe) {
    return Number.isFinite(importe) && importe > 0;
}

describe("importeValido", () => {
    it("acepta importes positivos", () => {
        expect(importeValido(100)).toBe(true);
    });

    it("rechaza cero", () => {
        expect(importeValido(0)).toBe(false);
    });
});

Consideraciones para producción

Los fragmentos de esta guía sirven para explicar conceptos, no como código listo para un sistema bancario real. En Java suele preferirse BigDecimal para cálculos financieros exactos. No se debe confiar solo en las validaciones del frontend: el backend debe volver a validar permisos, límites y datos.

Una transferencia real requiere transacciones de base de datos, controles de concurrencia, idempotencia, auditoría y mecanismos de reconciliación apropiados. Las APIs concretas de CELLS, ASO y sus controllers dependen de la versión; antes de implementarlas hay que verificar los paquetes, convenciones y documentación interna autorizada.

© Arturo Juárez Monroy