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.
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:
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:
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
Aún no hay comentarios.