pablo.mourato
PROJETS / CRÉNOÉTUDE DE CAS · 01 / 05

Créno.

Une application de prise de rendez-vous en ligne pour un salon de coiffure : le client réserve un créneau en deux clics, le commerçant pilote son planning. Conçue, développée, testée et déployée seul, du schéma SQL à la mise en ligne.

RÔLE
Conception, back-end, front-end, déploiement
CONTEXTE
Projet phare du portfolio · salon fictif, le Salon Camille à Blois
ANNÉE · DURÉE
2026 · 3 semaines
STACK
Java 21, Spring Boot 3.5, PostgreSQL 16, React 19, TypeScript
Page d'accueil de Créno pour le Salon Camille : « L'art de la coupe, réservé en un clic »
01/ — CONTEXTE

Le besoin.

Beaucoup de petits commerces prennent encore leurs rendez-vous par téléphone : le coiffeur décroche entre deux clients, le client rappelle s'il tombe sur la messagerie. Créno répond à ce problème à travers un salon fictif, le Salon Camille, pour montrer une application complète et utilisable, pas une simple maquette.

Je voulais un projet qui prouve le côté back : des règles métier réelles (horaires avec pause déjeuner, fermetures exceptionnelles, délai d'annulation), une base de données qui garantit la cohérence, une API sécurisée et documentée, des tests automatisés, et un vrai déploiement.

OBJECTIF 1Ne proposer que des créneaux réellement libres, calculés en direct
OBJECTIF 2Aucune double réservation, même si deux clients cliquent au même instant
OBJECTIF 3Un back-office simple pour les prestations, les horaires et le planning
02/ — ARCHITECTURE

Un front, une API,
une base.

Le front React appelle une API REST Spring Boot, seule à parler à PostgreSQL. L'authentification repose sur des jetons JWT signés par l'API, avec deux rôles (client et commerçant). Le schéma de la base est versionné avec Flyway, et chaque couche est déployée chez un hébergeur différent.

Front ReactReact 19 · TypeScript · TanStack Query · Tailwind · hébergé sur Vercel
↓ HTTPS + JWT↑ JSON · ProblemDetail
API Spring BootSpring Security · JPA · Flyway · OpenAPI · Docker sur Render
↓ JDBC↑ contraintes SQL
PostgreSQL 16Prestations, horaires, fermetures, rendez-vous · hébergé sur Neon
57 TESTS D'INTÉGRATION CONTRE UN VRAI POSTGRESQL (TESTCONTAINERS)
03/ — FONCTIONNALITÉS

Deux espaces,
un seul outil.

Côté client, réserver doit prendre moins d'une minute. Côté commerçant, l'essentiel doit se lire d'un coup d'œil.

Page de réservation : choix du jour, des horaires libres et récapitulatif
01

Créneaux en temps réel

L'API calcule les horaires libres à partir des plages d'ouverture, des pauses, des fermetures et de la durée de la prestation, par pas de 15 minutes.

Espace client avec les rendez-vous à venir et le tag « Nouveau »
02

Espace client

Rendez-vous à venir et historique, annulation en ligne jusqu'à 2 h avant, et un tag « Nouveau » sur les réservations récentes.

Planning du commerçant avec le résumé des nouveaux rendez-vous
03

Planning du commerçant

Vue par semaine, chiffre d'affaires prévu, taux d'absence, et à la connexion un résumé des rendez-vous pris depuis la dernière visite.

Trois écrans de Créno sur téléphone : accueil, réservation, rendez-vous
04

Pensé pour le mobile

Chaque écran est conçu d'abord pour le téléphone : c'est là que la plupart des clients réservent.

Documentation interactive de l'API dans Swagger UI
05

API documentée

Chaque route est décrite et testable dans Swagger UI, généré depuis le code. Les erreurs suivent le format standard ProblemDetail.

Transition de connexion affichant « Bonjour, Camille »
06

Transitions soignées

Connexion et déconnexion passent par un fondu : la session change sous le voile, sans écran intermédiaire ni redirection visible.

← FAITES DÉFILER →
04/ — DÉFIS TECHNIQUES

Ce qui a
résisté.

DÉFI 01

Calculer les créneaux libres

LE PROBLÈME

Un créneau libre dépend de beaucoup de choses : plages d'ouverture, fermetures exceptionnelles, rendez-vous existants, durée de la prestation, délai minimum avant réservation. Et tout doit rester juste en heure de Paris, changements d'heure compris.

MA SOLUTION

Une classe pure, sans Spring ni base de données, donc simple à tester unitairement. Elle raisonne en instants absolus : les dates sont calculées en heure de Paris puis stockées en UTC.

SlotCalculator.java — extrait
// Pour chaque plage d'ouverture du jour (ex. 9h-12h, puis 14h-19h)
for (OpeningRange range : ranges) {
    // Instants absolus : un jour de changement d'heure dure 23 h ou 25 h
    Instant rangeEnd = date.atTime(range.closesAt()).atZone(zone).toInstant();
    Instant start = date.atTime(range.opensAt()).atZone(zone).toInstant();
    while (!start.plus(serviceDuration).isAfter(rangeEnd)) {
        Instant end = start.plus(serviceDuration);
        if (!start.isBefore(notBefore) && isFree(start, end, busy)) slots.add(…);
        start = start.plus(step); // pas de 15 minutes
    }
}
DÉFI 02

Deux clients, un seul créneau

LE PROBLÈME

Si deux clients valident le même créneau au même instant, les deux vérifications côté Java peuvent passer avant que l'un ou l'autre n'enregistre : un contrôle applicatif seul ne suffit pas.

MA SOLUTION

Deux barrières : l'API recalcule les créneaux et refuse toute heure qui n'en fait pas partie, puis PostgreSQL garantit la règle avec une contrainte d'exclusion. Le second enregistrement échoue et devient une réponse 409. Un test lance 8 réservations simultanées : une seule réussit.

V1__init_schema.sql — extrait
-- Garde-fou ultime : deux RDV actifs ne peuvent jamais se chevaucher,
-- même quand deux requêtes arrivent au même instant.
CONSTRAINT ex_appointments_no_overlap EXCLUDE USING gist (
    tstzrange(start_at, end_at, '[)') WITH &&
) WHERE (status = 'BOOKED')
DÉFI 03

Deux minutes et demie de démarrage

LE PROBLÈME

Hébergée gratuitement, l'API s'endort après 15 minutes sans visite et ne dispose que d'une fraction de processeur : au réveil, le premier visiteur attendait 152 secondes.

MA SOLUTION

Une archive CDS générée pendant le build Docker, le compilateur JIT C1 seul et un ramasse-miettes série : 63 s en production. Côté front, un message prévient le visiteur pendant le réveil.

Dockerfile — extrait
# Pendant le build : on démarre l'appli une fois, sans base de données,
# et la JVM enregistre toutes les classes chargées dans app.jsa
RUN java -XX:ArchiveClassesAtExit=app.jsa \
      -Dspring.context.exit=onRefresh -jar creno.jar

# Au lancement réel, la JVM relit l'archive au lieu de tout recharger
ENV JAVA_TOOL_OPTIONS="-XX:TieredStopAtLevel=1 -XX:+UseSerialGC"
ENTRYPOINT ["java", "-XX:SharedArchiveFile=app.jsa", "-jar", "creno.jar"]
05/ — RÉSULTATS

En ligne.

57tests d'intégration, contre un vrai PostgreSQL
1 / 8réservation acceptée quand 8 clients visent le même créneau
−58 %de temps au démarrage à froid (152 s → 63 s)

L'application est en ligne avec des comptes de démonstration, commerçant et client : on peut réserver, annuler et gérer le planning. Le code est public, avec un README qui détaille les choix techniques et la sécurité (validation des entrées, CSP, rôles).

CE QUE J'EN RETIENS

Concevoir une architecture où les règles métier sont définies avant l’interface, garantir la cohérence des données directement au niveau de la base et valider le comportement de l’application sur un véritable environnement PostgreSQL. Le tout en s’adaptant aux limitations de performance imposées par un hébergement gratuit.