Niveau : Intermédiaire Technologies : Java 17+, Spring Boot 3.x, VAVR 0.10.4, Spring Data JPA (H2)
Vous êtes développeur.euse dans une startup FinTech appelée VavrBank. Vous devez implémenter le backend d’une application de gestion de comptes bancaires en appliquant les concepts vus en formation : Either, Option, Try, Validation.
Either
Option
Try
Validation
L’objectif est de ne jamais utiliser d’exceptions pour les erreurs métier, mais uniquement pour les erreurs techniques non prévisibles.
vavrbank/ ├── src/main/java/com/vavrbank/ │ ├── VavrBankApplication.java │ ├── config/ │ │ └── JacksonConfig.java │ ├── domain/ │ │ ├── Compte.java (entité JPA) │ │ ├── Transaction.java (entité JPA) │ │ └── TypeTransaction.java (enum : CREDIT, DEBIT) │ ├── error/ │ │ └── BanqueError.java (interface — à compléter) │ ├── repository/ │ │ ├── CompteRepository.java │ │ └── TransactionRepository.java │ ├── service/ │ │ ├── CompteService.java (à implémenter) │ │ └── ValidationService.java (à implémenter) │ ├── controller/ │ │ └── CompteController.java (à implémenter) │ └── dto/ │ ├── CreateCompteDTO.java │ ├── VirementDTO.java │ └── CompteResponseDTO.java └── src/main/resources/ └── application.properties ou application.yml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.vavr</groupId> <artifactId>vavr</artifactId> <version>0.10.4</version> </dependency> <dependency> <groupId>io.vavr</groupId> <artifactId>vavr-jackson</artifactId> <version>0.10.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>
application.properties
Pas besoin de base de données postgres
spring.datasource.url=jdbc:h2:mem:vavrbank spring.datasource.driver-class-name=org.h2.Driver spring.jpa.hibernate.ddl-auto=create-drop spring.h2.console.enabled=true spring.jpa.show-sql=true
JacksonConfig.java
package com.vavrbank.config; import com.fasterxml.jackson.databind.ObjectMapper; import io.vavr.jackson.datatype.VavrModule; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * configuration Jackson pour supporter les types VAVR (Option, Either,...) * dans la sérialisation/désérialisation JSON. */ @Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer vavrCustomizer() { return builder -> builder.modulesToInstall(new VavrModule()); } }
Compte
L’entité Compte doit avoir :
id
numero
proprietaire
solde
actif
true
dateCreation
Transaction
L’entité Transaction doit avoir :
compteSource
compteDestination
montant
type
CREDIT
DEBIT
dateTransaction
description
BanqueError
Implémentez la l’interface BanqueError avec les implémentations suivantes (records Java) :
interface BanqueError
public sealed interface BanqueError { // À implémenter avec record : // - CompteInexistant(String numero) // - SoldeInsuffisant(BigDecimal solde, BigDecimal montant) // - MontantInvalide(String raison) // - CompteBloque(String numero) // - NumeroDejaExistant(String numero) // - VirementMemeCompte(String numero) }
Indice : Utilisez record pour chaque implémentation. Exemple : record CompteInexistant(String numero) implements BanqueError {}
record
record CompteInexistant(String numero) implements BanqueError {}
Implémentez ValidationService avec les méthodes suivantes. Chaque méthode doit utiliser Validation<String, T> de VAVR.
ValidationService
Validation<String, T>
@Service public class ValidationService { /** * Valide le numéro de compte. * Règles : non null, non vide, correspond au format "FR76-XXXX" * (commence par "FR76-" et est suivi de chiffres) */ public Validation<String, String> validerNumero(String numero) { // À implémenter } /** * Valide le nom du propriétaire. * Règles : non null, non vide, au moins 2 caractères, max 100 caractères */ public Validation<String, String> validerProprietaire(String proprietaire) { // À implémenter } /** * Valide un montant de transaction. * Règles : non null, strictement positif, max 10 000 € */ public Validation<String, BigDecimal> validerMontant(BigDecimal montant) { // À implémenter } /** * Valide un DTO de création de compte complet. * Doit utiliser Validation.combine() pour accumuler toutes les erreurs. * Retourne Either<List<String>, CreateCompteDTO> */ public Either<List<String>, CreateCompteDTO> validerCreateCompte(CreateCompteDTO dto) { // À implémenter } }
Implémentez CompteService. Toutes les méthodes doivent retourner Either<BanqueError, T>.
CompteService
Either<BanqueError, T>
@Service @Transactional public class CompteService { /** * Crée un nouveau compte bancaire. * - Valider le DTO (utiliser ValidationService) * - Vérifier que le numéro n'existe pas déjà * - Sauvegarder et retourner le compte créé */ public Either<BanqueError, Compte> creerCompte(CreateCompteDTO dto) { // À implémenter } /** * Trouve un compte par son numéro. * - Utiliser Option.ofNullable() et .toEither() */ public Either<BanqueError, Compte> trouverParNumero(String numero) { // À implémenter } /** * Effectue un virement entre deux comptes. * Étapes à chaîner avec flatMap : * 1. Valider le montant * 2. Vérifier que les deux numéros sont différents * 3. Trouver le compte source * 4. Vérifier que le compte source est actif * 5. Vérifier que le solde source est suffisant * 6. Trouver le compte destination * 7. Vérifier que le compte destination est actif * 8. Effectuer le débit/crédit (avec Try pour les erreurs techniques) * 9. Sauvegarder la transaction */ public Either<BanqueError, Transaction> effectuerVirement(VirementDTO dto) { // À implémenter } /** * Bloque un compte. */ public Either<BanqueError, Compte> bloquerCompte(String numero) { // À implémenter } /** * Retourne l'historique des transactions d'un compte. */ public Either<BanqueError, List<Transaction>> getHistorique(String numero) { // À implémenter } }
Implémentez CompteController. Utilisez fold() pour transformer les Either en ResponseEntity.
CompteController
fold()
ResponseEntity
POST /api/comptes
GET /api/comptes/{numero}
POST /api/comptes/virement
PUT /api/comptes/{numero}/bloquer
GET /api/comptes/{numero}/historique
BanqueError.CompteInexistant
404 Not Found
BanqueError.SoldeInsuffisant
402 Payment Required
BanqueError.MontantInvalide
400 Bad Request
BanqueError.CompteBloque
403 Forbidden
BanqueError.NumeroDejaExistant
409 Conflict
BanqueError.VirementMemeCompte
200 OK
201 Created
Une fois l’application lancée, testez avec les commandes suivantes directement avec Swagger ou Bruno (ici j’ai mis ce dont vous avez besoin).
{ "numero": "FR76-0001", "proprietaire": "Alicia Duponti" }
{ "numero": "FR76-0002", "proprietaire": "Boby Lapointe" }
{ "numero": "FR76-0001", "proprietaire": "Charlie" }
{ "numeroSource": "FR76-0001", "numeroDestination": "FR76-0002", "montant": 500 }
{ "numeroSource": "FR76-0001", "numeroDestination": "FR76-0002", "montant": -100 }
{ "numeroSource": "FR76-0001", "numeroDestination": "FR76-9999", "montant": 100 }
{ "numeroSource": "FR76-0001", "numeroDestination": "FR76-0002", "montant": 10 }
Ajoutez un CommandLineRunner qui initialise des comptes et des transactions au démarrage pour faciliter les tests. L’interface CommandeLineRunner vous oblige à implémenter la maéthode run() pour y mettre votre code d’initialisation des entités destinées à la persistance. Spring comprend avec l’annotation @Configuration
CommandLineRunner
CommandeLineRunner
run()
@Configuration
Je vous donne :
package com.vavrbank.config; import com.vavrbank.dto.CreateCompteDTO; import com.vavrbank.dto.VirementDTO; import com.vavrbank.service.CompteService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.math.BigDecimal; /** * initialisation des données de démonstration au démarrage. * Utilise les méthodes du service qui retournent Either — * on utilise peek() pour logger les succès et left() pour logger les erreurs à titre d'exemple. * Souvenez-vous que peek() ne change rien au valeur, il ne sert qu'à générer de l'information pour nos logs. */ @Configuration @RequiredArgsConstructor @Slf4j public class DataLoader { @Bean CommandLineRunner initData(CompteService compteService) { return args -> { log.info("== Initialisation des données de démonstration =="); // Créer des comptes avec dépôt initial compteService.creerCompte(new CreateCompteDTO("FR76-0001", "Alicia Duponti", new BigDecimal("5000"))) .peek(c -> log.info("Compte créé : {} (solde: {}€)", c.getNumero(), c.getSolde())) .peekLeft(e -> log.warn("Erreur : {}", e)); compteService.creerCompte(new CreateCompteDTO("FR76-0002", "Boby Lapointe", new BigDecimal("3000"))) .peek(c -> log.info("Compte créé : {} (solde: {}€)", c.getNumero(), c.getSolde())) .peekLeft(e -> log.warn(" Erreur : {}", e)); compteService.creerCompte(new CreateCompteDTO("FR76-0003", "Charlie Chaplin", new BigDecimal("1500"))) .peek(c -> log.info("Compte créé : {} (solde: {}€)", c.getNumero(), c.getSolde())) .peekLeft(e -> log.warn("️ Erreur : {}", e)); // Effectuer quelques virements de démonstration compteService.effectuerVirement(new VirementDTO("FR76-0001", "FR76-0002", new BigDecimal("500"), "Remboursement déjeuner")) .peek(t -> log.info(" Virement effectué : {} → {} pour {}€", t.getCompteSource(), t.getCompteDestination(), t.getMontant())) .peekLeft(e -> log.warn("️ Erreur virement : {}", e)); compteService.effectuerVirement(new VirementDTO("FR76-0002", "FR76-0003", new BigDecimal("200"), "Cadeau anniversaire")) .peek(t -> log.info(" Virement effectué : {} → {} pour {}€", t.getCompteSource(), t.getCompteDestination(), t.getMontant())) .peekLeft(e -> log.warn("️ Erreur virement : {}", e)); log.info("=== Données de démonstration initialisées ==="); log.info("Console H2 : http://localhost:8080/h2-console (JDBC URL: jdbc:h2:mem:vavrbank)"); }; } }
Implémentez un endpoint GET /api/comptes qui retourne tous les comptes avec leur solde, en utilisant les collections VAVR.
GET /api/comptes
Ajoutez une option dans CreateCompteDTO pour un dépôt initial optionnel (montant de départ). Utilisez Option<BigDecimal> dans le DTO.
CreateCompteDTO
Option<BigDecimal>
Implémentez GET /api/stats retournant :
GET /api/stats
import io.vavr.control.Either; import io.vavr.control.Option; import io.vavr.control.Try; import io.vavr.control.Validation; import io.vavr.collection.List; import io.vavr.collection.Seq;
Bon courage ! N’oubliez pas : Right = succès (c’est la “bonne” réponse), Left = erreur.
Right
Left