Microservice · Signature Électronique

Une abstraction.
Deux providers.
Zéro interruption.

Un microservice Symfony 8 qui unifie YouSign et UnSign derrière une seule API REST, avec basculement automatique et gestion des signatures ordonnées.

Symfony 8 API Platform 4.3 YouSign v3 UnSign (fallback)
🖥️
Client
App métier
Microservice
Symfony 8
🔁
Orchestrateur
+ Circuit Breaker
YouSign
Primary
🔄
UnSign
Fallback
02 · Architecture

Les couches du microservice

Chaque couche a une responsabilité unique et ne connaît pas les suivantes

▣   Couche Client — Consommateurs externes

🖥️ Application Métier

Front-end, back-office, CRM...

REST JSON-LDJWT Bearer

⚙️ Autre Microservice

Service GED, workflow, contrats

Service-to-Service

🔔 Webhook Entrant

Callbacks YouSign / UnSign → événements de signature

HMAC-SHA256
↓   HTTPS · JSON-LD · JWT   ↓
⬡   API Platform 4.3 — Exposition REST

📄 SignatureRequest

Resource principale. Création, lecture, annulation.

POSTGETDELETE

👥 Signers

Signataires ordonnés. Validation contraintes.

order: 1..N@Assert\Valid

📎 Documents

Upload PDF. Base64 ou multipart.

POST /documents

🔔 Webhooks

Réception callbacks providers. HMAC vérifié.

POST /webhooks/{provider}
↓   State Processors · State Providers   ↓
◎   Processors — Pont API Platform ↔ Métier

CreateSignatureProcessor

Mappe Resource → DTO. Lance le workflow complet. Sauvegarde en base.

POST handler

CancelSignatureProcessor

Annule auprès du provider et met à jour le statut.

DELETE handler

SignatureStateProvider

Lit depuis la BDD + interroge le provider pour le statut live.

GET handler
↓   DTO purs · Sans dépendance framework   ↓
◈   Services — Cerveau du microservice

🎯 SignatureOrchestratorService

Point d'entrée unique. Applique le basculement. Ne connaît que l'interface.

Pattern Facade

⚡ CircuitBreakerService

CLOSED → OPEN → HALF-OPEN. Seuils configurables. Stocké en Redis.

Redis State

🔁 WorkflowExecutorService

Exécute les étapes : create → upload → addSigner → activate. Sauvegarde après chaque step.

Idempotent

📊 ProviderHealthMonitor

Heartbeat périodique via Symfony Scheduler. Métriques latence.

Scheduler
↓   Pattern Adapter — même interface, syntaxes différentes   ↓
⇌   Adapters — Traduction vers les providers

YouSignAdapter   PRIMARY

  • Implémente SignatureProviderInterface
  • API YouSign v3 (api.yousign.app)
  • ordered_signers + delivery_mode
  • Retry 3x sur timeout
  • HMAC-SHA256 webhook verify

🔄 UnSignAdapter   FALLBACK

  • Implémente SignatureProviderInterface
  • Même contrat d'interface
  • position: N (clé différente de YouSign)
  • Retry 3x sur timeout
  • HMAC-SHA256 webhook verify
↓   Doctrine ORM · Redis · Symfony Messenger   ↓
◉   Infrastructure

🗄️ PostgreSQL

signature_requests · signers · provider_logs

Doctrine ORM

⚡ Redis

Circuit Breaker state · Health cache · Rate limiting

Symfony Cache

📨 RabbitMQ

File async webhooks entrants et notifications sortantes

Symfony Messenger

📁 S3 / MinIO

Stockage temporaire PDF avant envoi au provider

Flysystem
03 · Scénarios

Les 4 cas de défaillance

Comment le microservice réagit selon le moment où YouSign tombe

Scénario 1 — Nominal
YouSign fonctionne parfaitement. Flux standard.
createRequest() → uuid-xyz créé chez YouSign
uploadDocument() → PDF envoyé
addSigner(Mourad, order:1) → signer-111
addSigner(Jean, order:2) → signer-222
activate() → email envoyé à Mourad ✉️
🎉 Client reçoit 201 · provider: yousign · status: ongoing
💥
Scénario 2 — YouSign KO dès le début
Circuit Breaker OPEN. Bascule immédiate sur UnSign.
createRequest() chez YouSign → timeout / 500
🔌 CircuitBreaker.recordFailure() → compteur: 3/3 → état: OPEN ⚠️
🔄 Bascule UnSign → recoverOnFallback() déclenché
createRequest() chez UnSign → procedure-abc créé
②③④⑤ upload + signers + activate → tout sur UnSign
🔄 Client reçoit 201 · provider: unsign · transparent pour lui
Scénario 3 — YouSign tombe au milieu
Demande partiellement créée. Reprise totale sur UnSign.
createRequest() step sauvegardé: REQUEST_CREATED
uploadDocument() step: DOCUMENTS_UPLOADED
addSigner(Mourad) → YouSign tombe ici
♻️ recoverOnFallback() → step remis à INIT · provider: unsign
①②③④⑤ Workflow COMPLET sur UnSign → repart de zéro
⚡ Client reçoit 201 avec un léger délai · provider: unsign
🚨
Scénario 4 — Les deux providers KO
Échec total. Réponse 503 + retry-after au client.
createRequest() chez YouSign → KO
🔄 Bascule sur UnSign → recoverOnFallback()
createRequest() chez UnSign → aussi KO
💾 workflowStep → FAILED → sauvegardé en base ⚠️
📢 AllProvidersUnavailableException → log + alerte équipe ⚠️
🚨 Client reçoit 503 · Retry-After: 60s · Message explicite
04 · Circuit Breaker

Mécanisme de basculement

3 états stockés en Redis · Partagés entre tous les pods

🟢
CLOSED
État normal.
Tous les appels passent par YouSign.
On compte les erreurs.
🔴
OPEN
YouSign court-circuité.
Tous les appels vont sur UnSign directement.
On attend le délai de récupération.
🟡
HALF-OPEN
Période de test.
1 appel de sonde vers YouSign.
Si succès → CLOSED. Si échec → OPEN.

Transitions d'état

CLOSED OPEN 3 erreurs consécutives en moins de 60 secondes
OPEN HALF-OPEN Après 60 secondes écoulées (recovery timeout)
HALF-OPEN CLOSED 2 succès consécutifs → YouSign réintégré
HALF-OPEN OPEN 1 échec pendant la période de test

Configuration Redis

cb.yousign.state
"closed"
État courant du circuit
cb.yousign.failures
2
Erreurs depuis dernier reset
cb.yousign.last_fail
1718012400
Timestamp dernière erreur
cb.yousign.successes
0
Succès consécutifs (HALF-OPEN)
05 · Multi-Signature

Signatures ordonnées

Mourad signe en premier, Jean signe après — géré par le provider

Chaîne de signature

👨‍💼
Mourad
order: 1 · priority
✉️ Email reçu
👨‍💻
Jean
order: 2 · waiting
⏳ En attente
📄
Document
status: ongoing
⏳ En cours

Timeline des événements

✉️
Email envoyé à Mourad (order: 1)
activate() → provider envoie automatiquement à order:1 uniquement · Jean ne reçoit rien
✍️
Mourad signe
Provider déclenche webhook → event: "signer.done" · signerEmail: mourad@ex.com
🔔
POST /webhooks/yousign reçu
HMAC vérifié → Message Messenger publié → Worker met à jour signer.status: signed
✉️
Email envoyé automatiquement à Jean (order: 2)
Géré par le provider — toi tu n'as RIEN à faire · C'est YouSign/UnSign qui gère la séquence
✍️
Jean signe
Provider déclenche webhook → event: "signer.done" · signerEmail: jean@ex.com
🎉
Signature complète · event: "signature_request.done"
Worker met à jour status: DONE · URL document signé disponible

YouSign · ordered_signers

  • ordered_signers: true au niveau requête
  • Ordre déduit de l'ordre d'insertion des signataires
  • Mourad inséré en 1er → reçoit l'email en 1er

UnSign · position: N

  • Champ explicite position: 1 sur Mourad
  • Champ explicite position: 2 sur Jean
  • Même résultat, syntaxe différente
06 · Workflow

Exécution étape par étape

Chaque étape est sauvegardée — reprise possible à tout moment

createRequest() → step: REQUEST_CREATED
Appel au provider primaire (YouSign). Retourne un providerRequestId (UUID).
Si échec → recordFailure() → si seuil atteint → bascule UnSign.
💾 flush() après cette étape
uploadDocument() → step: DOCUMENTS_UPLOADED
Upload du PDF en base64 ou multipart vers le provider.
Retourne un providerDocumentId stocké dans l'entity.
💾 flush() après cette étape
③④
addSigner() × N → step: SIGNERS_ADDED
Signataires triés par order ASC avant envoi.
Mourad (order:1) puis Jean (order:2).
Chaque providerSignerId sauvegardé dans SignerEntity.
💾 flush() après cette étape
activate() → step: ACTIVATED · status: ONGOING
Active la demande. Le provider envoie l'email au 1er signataire (Mourad).
C'est seulement ici que les emails partent.
💾 flush() après cette étape
💥
ProviderException levée à n'importe quelle étape
recoverOnFallback() est déclenché.
workflowStep remis à INIT · providerName → 'unsign'.
executeWorkflow() rejoué intégralement sur UnSign.
💾 flush() état FAILED si les deux KO
🔔
Webhooks → traitement asynchrone (Messenger)
signer.done → markAsSigned(email) · provider envoie email au suivant.
signature_request.done → status: DONE · URL document disponible.
07 · Stack Technique

Technologies utilisées

Toutes des versions stables et maintenues en 2025

🐘
PHP 8.4
php 8.4.x
Readonly classes, enums, fibers, named arguments
Symfony 8
symfony/symfony ^8.0
HttpClient, Messenger, Scheduler, Cache, Validator
🔷
API Platform 4.3
api-platform/core ^4.3
State Processors, State Providers, JSON-LD, OpenAPI auto
🗄️
PostgreSQL 16
doctrine/orm ^3
Entités, migrations, relations OneToMany
Redis 7
symfony/cache
État Circuit Breaker · partagé entre pods
📨
RabbitMQ
symfony/messenger
File async webhooks · workers indépendants
YouSign API v3
api.yousign.app/v3
Provider primaire · ordered_signers · HMAC webhooks
🔄
UnSign
Provider fallback
Provider de secours · même interface · position: N
📁
S3 / MinIO
league/flysystem
Stockage temporaire PDF avant upload provider

Structure du projet

src/
├── ApiResource/
│   ├── SignatureRequestResource.php
│   ├── SignerInput.php
│   └── SignerOutput.php
├── Provider/
│   ├── SignatureProviderInterface.php
│   ├── YouSignAdapter.php
│   └── UnSignAdapter.php
├── Service/
│   ├── SignatureOrchestratorService.php
│   ├── CircuitBreakerService.php
│   └── WorkflowExecutorService.php
├── Processor/
│   ├── CreateSignatureRequestProcessor.php
│   └── CancelSignatureRequestProcessor.php
├── Entity/
│   ├── SignatureRequestEntity.php
│   └── SignerEntity.php
├── DTO/
│   ├── SignatureRequestDTO.php
│   ├── SignerDTO.php
│   └── SignatureResultDTO.php
├── Enum/
│   ├── SignatureStatusEnum.php
│   ├── SignerStatusEnum.php
│   └── CircuitStateEnum.php