Migration de Spring Boot vers Go et YDB Serverless : le parcours d’un entraîneur de frappe
Un développeur expérimenté en Java et Spring Boot a migré l'entraîneur de frappe TypeStep vers le cloud serverless d’Yandex. En abandonnant les machines virtuelles payantes et les services similaires à ECS, il a opté pour Go au niveau du backend et YDB plutôt que PostgreSQL. Résultat : démarrages de conteneurs froids en quelques millisecondes, tout en restant dans le forfait gratuit avec 1 million d'unités de requête par mois.
Go se compile en binaire statique sans JVM ni chauffage JIT. Les conteneurs démarrent instantanément — essentiel pour les conteneurs serverless où chaque requête peut déclencher une instance nouvelle. Spring Boot avec GraalVM nécessite une configuration supplémentaire, peu compatible avec un budget serré.
YDB Serverless a remplacé PostgreSQL en raison du coût. 1 million d'unités de requête par mois est gratuit, mais le calcul des unités restait partiellement flou sans une lecture approfondie de la documentation.
Architecture sans couches inutiles
Le frontend tourne sur Next.js avec génération statique, stocké dans un stockage d'objets. Le backend est écrit en Go sous Docker, exécuté sur des conteneurs serverless. La base de données est YDB Serverless. Les requêtes circulent directement depuis le navigateur : les ressources statiques proviennent du stockage, les appels API vont aux conteneurs. Pas de passerelle API → réduction de la latence et des coûts.
// Exemple d'énumération pour les colonnes 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
}
Après avoir quitté JPA, concevoir manuellement les schémas et écrire des requêtes YQL semble être un pas en arrière. Chaque champ nécessite un DECLARE, des paramètres typés et un balayage manuel.
Repositories et requêtes YQL
Les repositories utilisent YQL avec transactions. Exemple de création d’un enregistrement :
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()
// ... balayer le résultat dans &result
return res.Err()
})
return result, err
}
Ajouter une colonne nécessite des mises à jour dans toutes les opérations : INSERT, UPDATE, SELECT. Contrairement à JPA, où les entités s’actualisent automatiquement.
- Avantages du SQL manuel : contrôle total sur les requêtes, pas de problèmes N+1.
- Inconvénients : verbeux, mappage de type manuel, gestion des énumérations.
- Comparaison avec JPA : JPA est plus simple pour les schémas basiques, mais cache les détails d’exécution.
Transactions dans YDB distribué
YDB ne prend pas en charge les clés étrangères, mais propose des transactions. Sans annotation @Transactional, la gestion doit être manuelle.
TransactionManager vérifie si une transaction existe déjà dans le contexte :
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)
})
}
Cela simule la propagation de Spring : soit démarrer une nouvelle transaction, soit réutiliser une existante. Élimine la duplication de code (Create vs CreateTx).
Les services et repositories injectent le manager. Plusieurs opérations dans une même transaction fonctionnent sans heurt.
Injection de dépendances manuelle et compilation
Sans IoC Spring, les dépendances sont assemblées dans main.go dans l’ordre : DB → repositories → services → handlers. Pas de réflexion, de proxy ou de boucle à runtime.
Avantages :
- Graphique de dépendances explicite.
- Pas de magie ni d’erreurs imprévues.
- Refactoring facile.
Inconvénients : boilerplate manuel pour chaque composant.
Gestion des erreurs avec contexte
En Go, les erreurs sont des valeurs sans traces de pile. Solution : préfixes uniques dans 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)
}
// ...
}
Dans les logs : ProcessItem: failed to find item abc123: ItemRepository.FindById: query failed: connection refused. La chaîne pointe précisément vers le point de défaillance.
Optimisation des unités de requête YDB
Le schéma initial reproduisait PostgreSQL : statistiques de lettres dans des lignes séparées, boucles INSERT/UPDATE par clé. Cela consommait rapidement les unités de requête.
Optimisations :
- Agrégation côté client avant insertion.
- Opérations groupées.
- Réévaluation des index et partitionnement.
Après ces corrections, nous restons dans le forfait gratuit.
Points clés
- Go offre des démarrages froids sous 1 seconde pour les conteneurs serverless, contre plusieurs secondes avec Spring Boot.
- YDB impose des YQL manuels et des paramètres DECLARE ; simplifiez les schémas pour économiser les unités de requête.
- TransactionManager simule @Transactional sans duplication de code.
- L’injection manuelle de dépendances est plus claire que Spring, mais exige une discipline dans main.go.
- Encapsulez les erreurs avec des préfixes pour des logs lisibles.
— Editorial Team
Aucun commentaire pour le moment.