Les plateformes de livraison ont un profil de trafic brutal : presque rien à 4 h du matin, puis un mur de commandes à 12 h et à 19 h. Le backend doit absorber ces pics sans perdre une seule commande, garder coursiers, marchands et clients synchronisés, et rester assez simple pour qu'une petite équipe puisse l'exploiter. Voici la forme que nous utilisons.
Commencez par la machine à états de la commande
Chaque bug de livraison que nous avons débogué était, à la racine, une transition d'état illégale. Nous rendons donc les transitions explicites et testables unitairement avant d'écrire le moindre contrôleur.
// Order state machine — explicit, testable transitions
public enum OrderStatus { Placed, Accepted, Preparing, ReadyForPickup, PickedUp, Delivered, Cancelled }
static readonly Dictionary<OrderStatus, OrderStatus[]> Allowed = new()
{
[OrderStatus.Placed] = new[] { OrderStatus.Accepted, OrderStatus.Cancelled },
[OrderStatus.Accepted] = new[] { OrderStatus.Preparing, OrderStatus.Cancelled },
[OrderStatus.Preparing] = new[] { OrderStatus.ReadyForPickup },
[OrderStatus.ReadyForPickup] = new[] { OrderStatus.PickedUp },
[OrderStatus.PickedUp] = new[] { OrderStatus.Delivered },
};
public void Transition(OrderStatus next)
{
if (!Allowed.TryGetValue(Status, out var ok) || !ok.Contains(next))
throw new InvalidOperationException($"{Status} -> {next} is not allowed");
Status = next;
_events.Add(new OrderStatusChanged(Id, next, DateTime.UtcNow));
}Chaque transition émet un événement de domaine. Ces événements pilotent tout le reste — notifications, affectation des coursiers, analytique — sans que le service de commandes sache qui écoute.
Publier les événements de façon fiable : l'outbox
Écrire en base puis publier sur un bus de messages est le problème classique de la double écriture : si la seconde étape échoue, vous avez une commande payée que personne ne livre. Le pattern outbox transactionnel le résout en écrivant l'événement dans la même transaction que la commande.
// Transactional outbox — publish events reliably
await using var tx = await db.Database.BeginTransactionAsync();
db.Orders.Update(order);
db.OutboxMessages.Add(new OutboxMessage("order.status-changed", JsonSerializer.Serialize(evt)));
await db.SaveChangesAsync();
await tx.CommitAsync();
// A background worker (Hangfire / hosted service) drains the outbox to Azure Service Bus or Amazon SQS.Des services, pas un monolithe — mais pas trop
Nous découpons généralement un backend de livraison en cinq services déployables : Commandes, Dispatch (appariement des coursiers et itinéraires), Catalogue (marchands et menus), Paiements et Notifications. Chacun possède ses données. Un découpage plus fin ajoute des sauts réseau et des coûts d'exploitation sans bénéfice réel pour une équipe de moins de vingt ingénieurs.
Correspondance des briques sur Azure et AWS
| Besoin | Microsoft Azure | Amazon Web Services |
|---|---|---|
| Conteneurs | Azure Kubernetes Service / Container Apps | Amazon EKS / ECS Fargate |
| Bus de messages | Azure Service Bus | Amazon SQS + SNS |
| Base relationnelle | Azure SQL / Azure Database for MySQL | Amazon RDS / Aurora |
| Cache & géospatial | Azure Cache for Redis | Amazon ElastiCache |
| Fichiers (preuve de livraison) | Blob Storage | Amazon S3 |
| Observabilité | Application Insights | CloudWatch + X-Ray |
Le code applicatif est identique sur les deux — .NET avec des abstractions au-dessus du bus et du stockage — et c'est précisément pourquoi nous recommandons de cantonner les appels SDK propres au cloud aux bordures du système.
Absorber le pic de midi
- Mettez à l'échelle sur la profondeur de file, pas sur le CPU. Les workers de dispatch s'ajustent au nombre de commandes non affectées.
- Mettez le catalogue en cache. Les menus changent rarement et sont lus en permanence ; Redis avec des TTL courts supprime 80 % de la charge de la base.
- Limitez le débit de la tablette marchand. Un restaurant qui rafraîchit chaque seconde pendant le coup de feu est un DDoS auto-infligé.
- Préchauffez avant le pic. Une montée en charge planifiée à 11 h 30 et 18 h 30 coûte moins cher qu'une montée réactive à 12 h 05.
ETA en direct et itinéraires
Les positions des coursiers transitent par des ensembles géospatiaux Redis ; les ETA proviennent de l'API Routes de Google Maps Platform et sont mis en cache par segment. Nous regroupons les appels — une requête pour plusieurs coursiers candidats — ce qui garde la facture cartographique prévisible.
En résumé
Un état explicite, des événements fiables, une poignée de services bien délimités et une mise à l'échelle pilotée par les files : c'est la recette derrière chaque plateforme de livraison que nous avons livrée. Le fournisseur cloud compte moins que la discipline.