CodeGym /Cursos /Spring Core /ContextFlow: servicios sin estado y sesión

ContextFlow: servicios sin estado y sesión

Spring Core
Nivel 11, Lección 4
Disponible

1. Problema: servicios con «memoria»

En ContextFlow la tentación es la misma que en cualquier proyecto pequeño: guardar currentOrderId, la lista de pasos u otro trozo temporal de datos directamente en un campo del servicio. Al principio todo funciona, estás contento, el gato está contento — y luego el singleton-bean empieza a recordar las operaciones anteriores y se convierte silenciosamente en un bloc de notas compartido para toda la aplicación. Incluso sin una capa web, esto hace que el comportamiento sea frágil: ya no basta con llamar a placeOrder() — también es importante saber qué quedó en sus campos después de la última vez.

2. El comportamiento vive mucho, los datos de la operación — no

Por lo tanto, el límite aquí es simple. OrderPlacementService, AuditService, NotificationDispatchService y otros servicios viven mucho tiempo y son responsables del comportamiento: coordinan pasos, llaman dependencias, aceptan parámetros. Pero los datos de un solo procesamiento de pedido — pasos del escenario, marcas temporales, banderas intermedias — deben vivir dentro del método o en un objeto separado de corta duración. De lo contrario, mezclamos «quién ejecuta el escenario» y «qué se ha acumulado dentro de una ejecución concreta».

3. Hacemos los servicios de ContextFlow sin estado (y no nos avergonzamos de ello)

Stateless aquí no significa «sin campos en absoluto». Los campos con dependencias permanecen: OrderStore, AuditWriter, NotificationSender. Lo que no debe permanecer son los datos de una llamada concreta — currentOrderId, currentChannel, la lista de pasos y todo lo que pertenece solo a una operación.

Si esos datos siempre quieren colocarse en un campo, ya es una señal: el servicio necesita un portador separado del estado temporal. Para una operación simple bastan las variables locales. Para un escenario más complejo, es más conveniente crear un OrderProcessingSession.

4. Diseñamos OrderProcessingSession: qué es y qué almacenar en ella

OrderProcessingSession no es una nueva entidad de dominio al nivel de Order. Es un objeto de aplicación de una sola operación: una especie de carpeta de trabajo donde se acumulan los pasos del procesamiento del pedido. No necesita almacenarse en OrderStore ni convertirse en el objeto principal del sistema.

Para un proyecto educativo, es suficiente una lista de pasos de procesamiento. Y si se quiere comprobar rápidamente que diferentes llamadas reciben diferentes objetos session, para el código demo basta con System.identityHashCode(...) — no es necesario inflar la clase solo para un diagnóstico. Esto ya es suficiente para que el estado de la operación sea explícito y deje de dispersarse por los campos de los servicios.

5. Registramos OrderProcessingSession como un prototype-bean

Aquí prototype no se necesita solo por otra anotación. Si solo necesitáramos una lista local de cadenas, un simple new sería más honesto. Pero OrderProcessingSession es una parte significativa del grafo de ContextFlow: queremos que se cree de nuevo para cada operación y que siga siendo una dependencia normal del contenedor. Si más tarde necesita sus propias configuraciones, un Clock u otro helper, el servicio no empezará a crearlo manualmente.

Por lo tanto, la versión final de este nodo en ContextFlow se ve así:

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype") // Importante: nueva instancia en cada solicitud al contenedor
class OrderProcessingSession {
    private final List<String> steps = new ArrayList<>();

    void addStep(String step) {
        steps.add(step);
    }

    List<String> steps() {
        return List.copyOf(steps);
    }
}

Este es exactamente el objeto que puede ser mutable: su estado vive una sola operación y no se comparte entre llamadas.

6. Sesión por operación a través de ObjectProvider

Entonces todo se reduce al momento de obtener la instancia. Si se inyecta OrderProcessingSession directamente en el constructor del servicio singleton, el contenedor la creará una vez al inicio — y no se obtendrá ninguna «sesión por operación». Por lo tanto, el servicio no mantiene la session en sí, sino un ObjectProvider<OrderProcessingSession>:

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
class OrderPlacementService {
    // Mantenemos el provider, no la session en sí
    private final ObjectProvider<OrderProcessingSession> sessions;

    OrderPlacementService(ObjectProvider<OrderProcessingSession> sessions) {
        this.sessions = sessions;
    }
}

De esta forma, el servicio de larga duración permanece sin estado, y el objeto de corta duración se obtiene justo cuando comienza la operación.

7. Integramos OrderProcessingSession en el escenario de creación de pedidos

Ahora el patrón se ensambla por completo: el servicio toma una nueva session al inicio de placeOrder(...), registra los pasos del procesamiento actual y después de salir del método ya no mantiene la referencia.

void placeOrder(CreateOrderCommand command) {
    // Nueva instancia para cada operación
    OrderProcessingSession session = sessions.getObject();

    // Los pasos del procesamiento actual viven dentro de session, no en los campos del servicio
    session.addStep("validate");
    session.addStep("save");
    session.addStep("notify");

    System.out.println(session.steps()); // [validate, save, notify]
}

Aquí es donde ambas ideas del día se encuentran simultáneamente: el servicio singleton permaneció stateless, y el estado de una operación se trasladó a un objeto prototype separado.

flowchart TD
    A["OrderPlacementService
singleton"] -->|has| P["ObjectProvider<OrderProcessingSession>"] A --> S1["OrderStore
singleton"] A --> N["NotificationDispatchService
singleton"] A --> AU["AuditService
singleton"] P -->|"getObject() per call"| OS["OrderProcessingSession
prototype (new each time)"]

Si se quiere hacer el ejemplo un poco más «parecido a la vida real», se puede mostrar la marca técnica de la instancia:

OrderProcessingSession session = sessions.getObject();
// Verificación técnica: diferentes llamadas tendrán un objeto distinto
System.out.println("session=" + System.identityHashCode(session)); // session=123456789 (ejemplo)

El valor en el comentario es solo un ejemplo: en diferentes ejecuciones el número será diferente. Pero el significado sigue siendo el mismo — en cada llamada llega una nueva instancia.

Y una disciplina importante: no traslade toda la lógica de orquestación a la session. El servicio sigue gestionando el escenario (crear pedido, guardar, notificar), mientras que la session sigue siendo la «carpeta con documentos» de una sola ejecución.

8. Verificamos en tiempo de ejecución: una llamada — una session

Entender algo en Spring es la mitad del trabajo. La otra mitad es ver con los ojos que el contenedor realmente hace lo que esperas. Por lo tanto, es útil hacer una pequeña comprobación en tu ScenarioRunner (que ya ejecuta los escenarios de la aplicación). No necesitamos una prueba completa ni una infraestructura separada — simplemente dos llamadas al mismo escenario y comparar las instancias de session.

Por ejemplo, si agregas temporalmente un método de diagnóstico en OrderPlacementService:

boolean newSessionEachTime() {
    // Dos solicitudes al provider deben devolver diferentes instancias prototype
    OrderProcessingSession a = sessions.getObject();
    OrderProcessingSession b = sessions.getObject();

    // Comparamos referencias: lo importante es que es otro objeto
    return a != b;
}

entonces en el runner se puede mostrar:

// Esperamos true: el provider devolvió dos session diferentes
System.out.println(orderPlacementService.newSessionEachTime()); // true

Esta es una pequeña «lupa de ingeniería»: te aseguras de que el provider realmente devuelve diferentes instancias, es decir, el scope funciona como esperamos.

Si quieres vincular la verificación a una operación real, puedes llamar a placeOrder(...) dos veces en el runner y mirar los registros de pasos. Lo principal es no intentar convertir identityHashCode en un «hecho de negocio», lo usamos solo como demostración.

Y otro efecto agradable: una vez que has implementado este enfoque de session, se vuelve mucho más fácil explicar al estudiante (y a ti mismo) la diferencia entre «estado de la operación» y «estado del servicio». El servicio debe ser predecible. Y la session es precisamente el lugar donde lo «temporal» está permitido.

9. Errores típicos al trabajar con sesiones prototype

Error n.º 1: almacenar OrderProcessingSession en un campo del servicio singleton.
En cuanto la session sobrevive al método, deja de ser el estado de una operación y se convierte en un objeto compartido.

Error n.º 2: llamar a sessions.getObject() una vez por adelantado y almacenar en caché el resultado.
Entonces prototype vuelve a convertirse en una única instancia durante toda la vida del servicio, solo que a través de un camino indirecto más complicado.

Error n.º 3: en lugar de ObjectProvider, usar ApplicationContext y llamar a getBean() desde el código de negocio.
Aquí es suficiente con ObjectProvider; el contexto en el servicio rápidamente convierte el código en un service locator y oculta las dependencias.

Error n.º 4: convertir el session-bean en un «nuevo servicio» al que se traslada toda la lógica de negocio.
A veces el estudiante piensa: «Ya que la session almacena pasos, que ella misma ejecute los pasos». Y de repente la session se convierte en un segundo OrderPlacementService, pero con estado. Esto es una peligrosa mezcla de responsabilidades. Deja que la orquestación permanezca en los servicios, y la session almacene solo datos temporales (pasos, marcas, advertencias acumuladas).

Error n.º 5: convertir objetos de dominio (Order, Customer) en prototype-beans.
Es un intento frecuente de «vincular todo a Spring»: ya que el contenedor sabe crear objetos, que cree también Order. Generalmente es innecesario. Los objetos de dominio son datos del escenario, se crean en el código de negocio, viven en memoria/almacenamiento, se serializan, etc. Al contenedor se le deben delegar los servicios y la infraestructura, no cada entidad del dominio.

1
Tarea
Spring Core, nivel 11, lección 4
Bloqueada
Sesión por cada procesamiento de pedido
Sesión por cada procesamiento de pedido
1
Tarea
Spring Core, nivel 11, lección 4
Bloqueada
Orquestación de pedido sin estado en el servicio
Orquestación de pedido sin estado en el servicio
1
Cuestionario/control
Spring Scope, nivel 11, lección 4
No disponible
Spring Scope
Ámbitos de vida de los beans
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION