Volver al inicio

Go y YDB Serverless en lugar de Spring Boot

Desarrollador migró aplicación de Spring Boot y PostgreSQL a Go y YDB Serverless en Yandex Cloud. Describió desafíos de SQL manual, transacciones, DI y optimización de RU para niveles gratuitos.

Go + YDB: ajustándose al nivel gratuito de Yandex Cloud
Advertisement 728x90

Migrar de Spring Boot a Go y YDB Serverless: El camino de un entrenador de escritura

Un desarrollador con experiencia en Java y Spring Boot migró el entrenador de escritura TypeStep a Cloud de Yandex Serverless. Abandonar máquinas virtuales pagadas y servicios similares a ECS llevó a elegir Go para el backend y YDB en lugar de PostgreSQL. Resultado: arranques de contenedores fríos en milisegundos, permaneciendo dentro del plan gratuito con 1 millón de Unidades de Petición por mes.

Go compila en un binario estático sin JVM ni calentamiento JIT. Los contenedores se inician instantáneamente—crucial para Contenedores Serverless donde cada solicitud puede desencadenar una nueva instancia. Spring Boot con GraalVM requiere configuraciones adicionales, lo cual no encajaba con el presupuesto ajustado.

YDB Serverless reemplazó a PostgreSQL por cuestiones de costo. 1 millón de RU/mes es gratuito, pero el cálculo de unidades permaneció parcialmente confuso sin revisar profundamente la documentación.

Google AdInline article slot

Arquitectura sin capas innecesarias

El frontend funciona con Next.js con generación estática, almacenado en Almacenamiento de Objetos. El backend es Go en Docker sobre Contenedores Serverless. La base de datos es YDB Serverless. Las solicitudes fluyen directamente desde el navegador: activos estáticos desde el almacenamiento, llamadas API a los contenedores. Sin API Gateway, se reduce latencia y costos.

// Ejemplo de enum para columnas de YDB

type StageType string

const (
    Lowercase   StageType = "lowercase"
    Uppercase   StageType = "uppercase"
    Punctuation StageType = "punctuation"
    Numbers     StageType = "numbers"
)

func (s *StageType) Scan(value interface{}) error {
    return ScanStringEnum(s, value, "StageType")
}

func (s *StageType) Value() (driver.Value, error) {
    return string(*s), nil
}

Tras dejar JPA, diseñar manualmente el esquema y escribir consultas YQL parece un paso atrás. Cada campo requiere DECLARE, parámetros tipados y escaneo manual.

Repositorios y consultas YQL

Los repositorios usan YQL con transacciones. Ejemplo de creación de un registro:

Google AdInline article slot
func (r *ItemRepository) CreateItem(ctx context.Context, item Item) (*Item, error) {
    var result *Item
    err := r.txManager.DoTx(ctx, func(ctx context.Context, tx table.TransactionActor) error {
        res, err := tx.Execute(ctx, `
            DECLARE $id AS Utf8;
            DECLARE $name AS Utf8;
            DECLARE $status AS Utf8;
            DECLARE $created_at AS Datetime;

            INSERT INTO items (id, name, status, created_at)
            VALUES ($id, $name, $status, $created_at)
            RETURNING id, name, status, created_at;
        `,
            table.NewQueryParameters(
                table.ValueParam("$id", types.UTF8Value(item.Id)),
                table.ValueParam("$name", types.UTF8Value(item.Name)),
                table.ValueParam("$status", types.UTF8Value(string(item.Status))),
                table.ValueParam("$created_at", types.DatetimeValueFromTime(item.CreatedAt)),
            ),
        )
        if err != nil {
            return fmt.Errorf("failed to execute CreateItem: %w", err)
        }
        defer res.Close()

        // ... escanear resultado en &result
        return res.Err()
    })
    return result, err
}

Añadir una columna requiere actualizaciones en todas las operaciones: INSERT, UPDATE, SELECT. A diferencia de JPA, donde las entidades se actualizan automáticamente.

  • Ventajas del SQL manual: Control total sobre las consultas, sin problemas N+1.
  • Desventajas: Verbosidad, mapeo manual de tipos, enums.
  • Comparación con JPA: JPA es más sencillo para esquemas básicos pero oculta detalles de ejecución.

Transacciones en YDB distribuido

YDB no soporta claves foráneas pero ofrece transacciones. Sin @Transactional, su gestión debe ser manual.

TransactionManager verifica si existe una transacción en el contexto:

Google AdInline article slot
func (t *TransactionManager) DoTx(ctx context.Context, fn func(ctx context.Context, tx table.TransactionActor) error) error {
    if tx, ok := TxFromContext(ctx); ok {
        return fn(ctx, *tx)
    }
    return t.ydb.NativeDriver.Table().DoTx(ctx, func(ctx context.Context, tx table.TransactionActor) error {
        ctx = context.WithValue(ctx, txKey{}, &tx)
        return fn(ctx, tx)
    })
}

Esto emula la propagación de Spring: iniciar una nueva transacción o reutilizar una existente. Elimina duplicación de código (Create vs CreateTx).

Los servicios y repositorios inyectan el gestor. Operaciones múltiples en una sola transacción funcionan sin problemas.

Inyección de dependencias manual y compilación

Sin IoC de Spring, las dependencias se ensamblan en main.go en orden: DB → repositorios → servicios → manejadores. Sin reflexión, proxies ni bucles en tiempo de ejecución.

Ventajas:

  • Gráfico de dependencias explícito.
  • Sin magia ni errores inesperados.
  • Fácil refactorización.

Desventajas: boilerplate manual para cada componente.

Manejo de errores con contexto

Los errores en Go son valores sin trazas de pila. Solución: prefijos únicos en fmt.Errorf.

func (s *ItemService) ProcessItem(ctx context.Context, id string) error {
    item, err := s.repo.FindById(ctx, id)
    if err != nil {
        return fmt.Errorf("ProcessItem: failed to find item %s: %w", id, err)
    }
    // ...
}

En los logs: ProcessItem: failed to find item abc123: ItemRepository.FindById: query failed: connection refused. La cadena apunta exactamente al punto de fallo.

Optimización de Unidades de Petición en YDB

El esquema inicial imitaba a PostgreSQL: estadísticas de letras en filas separadas, bucles INSERT/UPDATE por clave. Esto consumía rápidamente las RU.

Optimizaciones:

  • Agregar datos del lado cliente antes de la inserción.
  • Operaciones por lotes.
  • Reevaluar índices y particionamiento.

Tras las correcciones, seguimos dentro del plan gratuito.

Conclusiones clave

  • Go ofrece arranques fríos bajo 1 segundo en Contenedores Serverless frente a segundos con Spring Boot.
  • YDB exige YQL manual y parámetros DECLARE; simplifica los esquemas para ahorrar RU.
  • TransactionManager emula @Transactional sin duplicación de código.
  • La inyección manual de dependencias es más clara que Spring, pero requiere disciplina en main.go.
  • Envuelve errores con prefijos para logs legibles.

— Editorial Team

Advertisement 728x90

Leer después