Portrait de Patrice GADEGBEPatrice GADEGBE

Soccercup

Soccercup est une application de gestion de tournois de football amateur, dans laquelle un organisateur compose ses poules, planifie les rencontres puis saisit les buts et les cartons, le serveur se chargeant de recalculer les scores et les classements.

Rôle
Auteur de l’application mobile et auteur principal de l’API, à laquelle un second développeur a contribué
Période
De juin 2025 à janvier 2026, pour l’essentiel en juin et juillet 2025
État
Prototype. Prototype fonctionnel, non publié
Technologies
Java 21, Spring Boot 3.5, PostgreSQL 16, Flutter, flutter_bloc
Dépôts
Deux
Architecture de SoccercupUne application Flutter appelle en JSON une API Spring Boot, qui enregistre ses données dans PostgreSQL et les images téléversées sur le disque.ApplicationFlutter, flutter_bloc14 cubits, 11 routesAPISpring Boot 3.5, Java 2135 points d’accèsPostgreSQL 16schéma tenu par HibernateJSON, multipartJPADisqueimages téléversées
Figure 1. Architecture de Soccercup. L’application ne reçoit rien du serveur en dehors de ses propres requêtes.

Contexte

Soccercup s’adresse à l’organisateur d’un tournoi amateur : une coupe y est définie par son nombre de poules, son nombre de tours de phase finale et son barème de points, après quoi les équipes sont réparties dans les poules et les rencontres planifiées.

L’application a d’abord été développée seule, à partir de la fin du mois de juin 2025, contre des données simulées. L’API a été écrite ensuite, en une dizaine de jours, du 13 au 23 juillet 2025.

Problème

Dans un tournoi, une saisie n’est jamais définitive. Un but est attribué à la mauvaise équipe, un carton est retiré après discussion, et chacune de ces corrections doit se retrouver à la fois dans le score de la rencontre, dans la ligne de l’équipe au sein de sa poule et dans le rang qu’elle y occupe. Tenir ces trois valeurs à la main, ou les modifier chacune de son côté, expose à ce qu’un classement affiché ne corresponde plus aux rencontres jouées.

Une coupe enchaîne par ailleurs deux phases de nature différente, les poules puis les tours à élimination, que l’application doit présenter sous cinq vues distinctes sans que le serveur ait à connaître ces vues.

Réponse apportée

Un journal d’événements pour seule saisie

L’organisateur ne saisit ni score ni classement. Il enregistre des événements de match, c’est-à-dire un but, un carton jaune ou un carton rouge, et le serveur recalcule la coupe entière après chaque création, modification ou suppression d’un événement : il recompte les événements de chaque rencontre, reconstruit la ligne de chaque équipe dans sa poule, puis reclasse chaque poule. Un but corrigé ne peut donc pas laisser derrière lui un score ou un tableau périmé.

Un départage explicite

Le classement d’une poule suit un ordre écrit dans le code plutôt que laissé à l’appréciation de l’organisateur : les points, puis la différence de buts, les buts marqués, le plus petit nombre de cartons rouges et enfin le plus petit nombre de cartons jaunes. Le barème appartient à la coupe, avec trois points par victoire, un par match nul et aucun par défaite à défaut d’autre choix, et seules les rencontres de poule déjà commencées sont comptées.

teamGroups.sort((tg1, tg2) -> {
    // Compare by points (descending)
    int cmp = Integer.compare(tg2.getPoints(), tg1.getPoints());
    if (cmp != 0)
        return cmp;
    // Compare by goal difference (scored - conceded, descending)
    int tg1GoalDiff = tg1.getScoredGoal() - tg1.getConcededGoal();
    int tg2GoalDiff = tg2.getScoredGoal() - tg2.getConcededGoal();
    cmp = Integer.compare(tg2GoalDiff, tg1GoalDiff);
    if (cmp != 0)
        return cmp;
    // Compare by scored goals (descending)
    cmp = Integer.compare(tg2.getScoredGoal(), tg1.getScoredGoal());
    if (cmp != 0)
        return cmp;
    // Compare by red cards (ascending, fewer is better)
    cmp = Integer.compare(tg1.getRedCards(), tg2.getRedCards());
    if (cmp != 0)
        return cmp;
    // Compare by yellow cards (ascending, fewer is better)
    cmp = Integer.compare(tg1.getYellowCards(), tg2.getYellowCards());
    return cmp;
});
Extrait 1. Ordre de départage au sein d’une poule, dans le service qui recalcule la coupe.

Une seule table pour les deux phases

Les rencontres de poule et celles de phase finale partagent une même table, où trois colonnes indiquent la phase, la journée et la position dans le tableau. L’application ne demande qu’une liste de rencontres par coupe, qu’elle découpe elle-même en résultats et calendrier des poules, en tableau, résultats et calendrier de la phase finale. La coupe suit de son côté quatre états, de la préparation à la clôture, et la phase finale disparaît de l’affichage lorsqu’une coupe n’en prévoit aucune.

Limites et perspectives

L’API n’est protégée par aucune authentification, de sorte que tout client peut modifier n’importe quelle coupe. Ce choix suffisait à un prototype et interdit en l’état un usage ouvert. L’adhésion d’un joueur à une équipe par un code d’invitation est par ailleurs simulée dans l’application : le serveur produit bien ce code, sans offrir encore le point d’accès qui permettrait de s’en servir.

Le recalcul complet de la coupe à chaque événement est simple à raisonner, mais il coûte une requête par rencontre et par compteur et ne s’exécute pas dans une transaction ; il conviendra de le rendre incrémental si une coupe dépasse quelques dizaines de rencontres. Le rafraîchissement des écrans repose enfin sur un bus d’événements interne à l’application, si bien qu’un second téléphone ne voit une modification qu’à son prochain chargement.

Les tests se réduisent à un test de démarrage côté serveur, et l’application n’en compte aucun. Le schéma de la base est géré par Hibernate sans outil de migration, ce qui devra changer avant toute mise en service réelle.

Retour aux autres projets