07 — Ingénierie logicielle
Du C proche du silicium au TypeScript à l’edge
Je choisis le langage selon le mode de défaillance que je cherche à éviter.
Rust là où la concurrence doit être prouvablement correcte, Go là où les services réseau doivent rester simples, C près du matériel, Python et TypeScript là où les modèles et les gens rencontrent le système — choisis délibérément, maîtrisés en profondeur.
La mesure honnête d’un ingénieur n’est pas la stack sur un CV — c’est ce qui a survécu au contact de vrais utilisateurs. La mienne, ce sont onze ans de cela.
Depuis plus de onze ans, je livre des applications mobiles à de vrais utilisateurs sur iOS, Android et Windows. Ce chiffre est le titre auquel je fais le plus confiance, parce que livrer à un appareil dans la poche de quelqu’un est impitoyable : il n’y a pas d’environnement de staging pour un crash sur le téléphone d’un inconnu, pas de rollback pour une fonction qui déroute celui qui l’utilise. Une décennie de cela enseigne une discipline précise — fragmentation des versions, comportement hors ligne, budgets de batterie et de mémoire, et l’accumulation lente du goût de ce qu’il faut laisser de côté.
Derrière chacune de ces applications se trouvaient un backend et l’infrastructure cloud en dessous. La pratique n’est donc pas seulement le devant de la vitre ; elle court de l’événement tactile vers le bas, à travers l’API, la file, le magasin de données et le conteneur, jusqu’au noyau sur lequel le conteneur tourne. J’ai passé assez de temps à chaque couche pour savoir où sont les coutures, et pour concevoir de sorte que les coutures ne se déchirent pas sous charge.
Ce que j’ai ajouté depuis, c’est de la profondeur en langages systèmes et en livraison multicloud : Rust et Go pour les cœurs qui doivent être corrects et rapides, Docker et Kubernetes pour la livraison, et un refus délibéré de verrouiller quoi que ce soit à un seul fournisseur. Le reste de cette page est la forme précise de cela — langages, plateformes, clouds, et les petits outils que je construis pour moi en chemin.
années à livrer des applications mobiles à de vrais utilisateurs
plateformes mobiles en production : iOS, Android, Windows
clouds exploités sans verrouillage fournisseur : GCP, Azure, AWS
langages principaux maîtrisés en profondeur : Rust, Go, Python, TypeScript
Une stack choisie par couche, pas par mode.
Diagramme en couches des langages systèmes
Chaque langage gagne sa place sur la couche où ses garanties comptent.
Je ne recours pas à un langage favori en y pliant chaque problème. Le diagramme se lit du silicium vers le haut : C et C++ là où le code touche le matériel, Rust pour les cœurs concurrents et sûrs en mémoire, Go pour la couche réseau concurrente, puis Python et TypeScript à la surface où vivent l’IA, les données et les utilisateurs.
Les largeurs des barres donnent un sens approximatif de l’étendue d’usage à travers mon travail — pas un benchmark, juste là où le volume de code tend à se situer.
- Les couches basses achètent déterminisme et sûreté
- Les couches hautes achètent vitesse et portée
- La frontière est un choix délibéré, fait par projet
À quoi le code ressemble vraiment.
Un pool de workers borné en Go — répartir le travail entre goroutines, collecter les résultats via un channel, et respecter l’annulation. Le motif apparaît sous presque tous les services réseau que j’écris.
// FanOut runs fn across at most n goroutines, streaming results
// back on a channel and stopping early if ctx is cancelled.
func FanOut[T, R any](ctx context.Context, in []T, n int, fn func(T) R) []R {
jobs := make(chan int)
out := make([]R, len(in))
var wg sync.WaitGroup
for w := 0; w < n; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := range jobs {
select {
case <-ctx.Done():
return
default:
out[i] = fn(in[i])
}
}
}()
}
for i := range in {
select {
case <-ctx.Done():
close(jobs)
wg.Wait()
return out
case jobs <- i:
}
}
close(jobs)
wg.Wait()
return out
} Là où va vraiment l’ingénierie.
Choisir le langage selon le mode de défaillance que j’évite.
Rust, c’est là où la justesse sous concurrence est non négociable. Le borrow checker n’est pas une taxe — c’est une preuve à la compilation qu’une classe de courses de données et d’erreurs use-after-free ne peut pas se produire, ce qui est exactement ce que je veux pour un cœur critique en performance qui doit tourner sans surveillance.
Go, c’est là où je veux des services réseau concurrents que je peux raisonner vite : goroutines et channels pour le modèle de concurrence, le profileur pprof intégré pour trouver le chemin chaud, et un binaire statique qui se déploie proprement dans un conteneur. C et C++ restent réservés au travail proche du matériel — firmware, SoC embarqué, le code qui repose directement sur la table des registres.
- Rust — ownership, lifetimes, abstractions à coût nul, concurrence sans peur
- Go — goroutines, channels, annulation par context, profilage pprof
- C / C++ — firmware embarqué, contrôle proche du matériel, timing déterministe
- Bash et SQL en production, sur PostgreSQL et BigQuery
Onze années de livraison à de vrais appareils, pas à des simulateurs.
Sur iOS je travaille en Swift et Objective-C avec SwiftUI pour les nouvelles surfaces, Core Data pour la persistance, Core Animation pour le mouvement, et SiriKit pour les intents vocaux. Sur Android, c’est Kotlin et Java contre l’Android SDK, avec la discipline de fragmentation des versions qui vient du support de nombreuses versions d’OS sur le terrain.
En multiplateforme, je recours à React Native (avancé) ou Flutter (avancé) quand une base de code partagée fait gagner sa place, et j’ai livré avec Xamarin à un niveau opérationnel. Derrière les applications se trouvent de vrais backends — Node/Express, Spring Boot, Django/Flask, Ruby on Rails, ou Go avec Gin et Gorm — exposant REST et GraphQL.
- iOS — Swift, Objective-C, SwiftUI, Core Data, Core Animation, SiriKit
- Android — Kotlin, Java, Android SDK sur de nombreuses versions d’OS
- Multiplateforme — React Native, Flutter (avancé), Xamarin (opérationnel)
- Données — SQLite, Redis, ORMs (Sequelize, Mongoose) ; API REST et GraphQL
Portable par conception, pas marié à un seul fournisseur.
Je garde les charges de travail portables pour que le cloud reste une commodité plutôt qu’une dépendance. Sur GCP j’utilise Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery et Firestore. Sur Azure, AKS, Functions, Cognitive Services et Container Apps. Sur AWS, Lambda, ECS/EKS, S3, RDS et SQS/SNS derrière CloudFront.
À l’edge je fais tourner Cloudflare Workers et Vercel Edge ; pour les produits qui évoluent vite je m’appuie sur Supabase ou Firebase comme backend. Tout est livré par conteneurs — Docker et Kubernetes — avec CI/CD sur GitHub Actions ou GitLab CI et une infrastructure définie comme du code, de sorte qu’un environnement est reproductible plutôt que construit à la main.
- GCP — Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery, Firestore
- Azure — AKS, Functions, Cognitive Services, Container Apps
- AWS — Lambda, ECS/EKS, S3, RDS, SQS/SNS, CloudFront
- Edge et BaaS — Cloudflare Workers, Vercel Edge, Supabase, Firebase
Des systèmes distribués qui restent corrects pendant que les choses bougent.
Le travail de backend, c’est là où la pensée systèmes paie. Je conçois autour de files de messages et d’une architecture orientée événements pour que les composants restent découplés, et autour de systèmes temps réel où WebSocket et la communication au niveau socket portent l’état entre client et serveur avec une faible latence.
Les protocoles de service à service, l’idempotence et la contre-pression sont des préoccupations de premier ordre, pas des arrière-pensées. L’objectif est une topologie qui se dégrade avec grâce — un service qui tombe devrait ralentir le système, pas l’arrêter.
- Files de messages et architecture orientée événements
- Systèmes temps réel sur WebSocket et communication au niveau socket
- Protocoles de service à service, idempotence, contre-pression
- Coordination distribuée avec dégradation gracieuse
Une application, trois clouds, aucun verrouillage.
Une charge de travail que je garde portable : conteneurs et infrastructure comme code au centre, déployable vers le fournisseur dans lequel une mission vit déjà. Le cloud est une couche commodité, pas une dépendance.
GCP
Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery, Firestore — mon choix par défaut pour les charges d’IA et la livraison gérée de conteneurs.
Azure
AKS, Functions, Cognitive Services, Container Apps — utilisé là où une organisation vit déjà dans l’écosystème Microsoft.
AWS
Lambda, ECS/EKS, S3, RDS, SQS/SNS derrière CloudFront — l’étendue de primitives pour les stacks de production traditionnels.
Edge
Cloudflare Workers et Vercel Edge — logique qui tourne près de l’utilisateur, pour la latence et la portée mondiale.
BaaS
Supabase et Firebase (Auth, Firestore, Realtime DB) — quand un produit a besoin d’un backend en jours, pas en semaines.
Livraison
Docker et Kubernetes, GitHub Actions et GitLab CI, infrastructure comme code — des environnements reproductibles de bout en bout.
Le cloud devrait être une commodité que je peux échanger, jamais une dépendance qui possède le produit.
Une chaîne d’outils Linux personnelle et continue
Chaque friction du flux de travail devient un petit programme.
Je fais tourner Linux au niveau administrateur — réglage du noyau, durcissement par sysctl, unités systemd, runtimes de conteneurs, et le réseau en dessous. Mais la partie qui définit ma façon de travailler est plus modeste : au fil des ans j’ai écrit des dizaines de mes propres utilitaires en ligne de commande, un à la fois, chacun né d’une friction rencontrée plus d’une fois.
Une tâche faite deux fois est candidate à un outil. Le résultat est une station de travail qui épouse mes mains plutôt que les valeurs par défaut — et, plus important, l’instinct de celui qui construit l’outil plutôt que de tolérer l’écart. Cet instinct est le même que celui qui finit par se livrer dans les produits.
- Réglage du noyau, durcissement par sysctl, systemd, runtimes de conteneurs, réseau
- Des dizaines d’utilitaires CLI maison, chacun résolvant une friction récurrente
- Le réflexe de celui qui construit ses outils, pas seulement de celui qui les utilise
Ce que j’apporte à une construction.
Stack d’ingénierie
- Langages systèmes principaux
- Rust, Go, C/C++
- Langages de haut niveau
- Python, TypeScript/JS
- Mobile — natif
- Swift, Obj-C, Kotlin, Java
- Mobile — multiplateforme
- React Native, Flutter
- Backends
- Node, Spring, Django, Rails, Go
- Styles d’API
- REST, GraphQL
- Magasins de données
- PostgreSQL, BigQuery, SQLite, Redis
- Conteneurs
- Docker, Kubernetes
- CI/CD
- GitHub Actions, GitLab CI
- Linux
- Niveau administrateur
Rien de tout cela n’est une liste de souhaits. Chaque ligne est un outil avec lequel j’ai livré — choisi, dans un projet donné, parce qu’il était la bonne réponse pour cette couche et ce mode de défaillance.
Le fil conducteur à travers tout cela est le même tempérament : construire le système avant le fichier, concevoir pour les coutures, garder la livraison reproductible, et refuser les dépendances qui posséderaient le produit plus tard.
Découplé par message, pas par espoir.
Une topologie producteur / broker / consommateur
Les composants se parlent à travers un broker, pour qu’une défaillance ralentisse le système au lieu de l’arrêter.
Quand les services s’appellent directement, une seule dépendance lente se transforme en cascade. Je conçois plutôt autour d’un broker d’événements : les producteurs publient des faits sur ce qui s’est produit, le broker les conserve durablement, et les consommateurs traitent à leur propre rythme. Le producteur ne se bloque jamais sur le consommateur, et un consommateur qui prend du retard vide son arriéré sans perdre de travail.
Les événements deviennent la source de vérité de ce qui s’est produit — une file de messages morts attrape ce qui ne peut être traité, les réessais sont bornés et idempotents, et un nouveau consommateur peut s’abonner au même flux sans que personne ne change le producteur. C’est la propriété que j’achète : l’évolution indépendante de parties qui n’ont jamais à se connaître.
- Les producteurs publient, les consommateurs s’abonnent — aucun couplage direct
- Un broker durable absorbe les rafales ; les consommateurs traitent à leur rythme
- File de messages morts et réessais bornés et idempotents en cas d’échec
D’un commit à la production, l’artefact promu vers l’avant.
Un pipeline CI/CD que je traite comme une infrastructure non négociable. L’image est construite une fois et promue inchangée à travers chaque porte, de sorte que ce qui tourne en production est octet pour octet ce qui a passé les tests.
Du commit au canary
- 01 Commit Push sur une branche. Les hooks pré-commit lancent le formatage et le lint en local, pour que l’évident soit attrapé avant même que la CI ne démarre.
- 02 Build et test Image de conteneur construite une fois, tests unitaires et d’intégration lancés contre elle, seuil de couverture imposé. Le même artefact est promu vers l’avant.
- 03 Analyse Analyse statique, audit des dépendances et scan de vulnérabilités d’image. Une analyse en échec bloque la fusion, pas la mémoire d’un relecteur humain.
- 04 Staging Déployer l’image signée vers un environnement de staging qui reflète la production, lancer des smoke tests et un court soak.
- 05 Promouvoir Déployer en production derrière un canary ou un bascule blue-green, surveiller les métriques, et garder la version précédente à une commande de distance.
État porté sur un socket, avec les modes de défaillance gérés.
Un maillage de services sur socket
Une passerelle WebSocket à l’avant, des connexions à état derrière, un magasin partagé pour la durabilité.
Le travail temps réel vit ou meurt par les parties peu glorieuses : ce qui se passe quand la connexion d’un client tombe au milieu d’un message, quand un consommateur prend du retard sur le producteur, quand le même événement arrive deux fois. Je termine les connexions WebSocket et socket à une passerelle, j’achemine chacune vers un handler à état, et je garde l’état durable dans un magasin partagé pour qu’un client qui se reconnecte puisse reprendre plutôt que redémarrer.
La contre-pression est explicite — des buffers bornés avec une politique définie de rejet ou de fusion, jamais une file sans borne qui mange silencieusement la mémoire jusqu’à ce que le processus meure. La présence est suivie par heartbeats et clés à TTL, et la livraison est idempotente pour qu’un transport at-least-once ne devienne pas un comportement at-least-twice pour l’utilisateur.
- Passerelle WebSocket / socket termine et achemine les connexions
- Handlers à état par connexion ; magasin partagé pour la durabilité
- Heartbeats, présence à TTL, reprise à la reconnexion, livraison idempotente
Primitives temps réel
- Transport
- WebSocket, sockets TCP bruts, flux gRPC
- Fan-out
- Pub/Sub, canaux Redis, SSE pour un seul sens
- Contre-pression
- Buffers bornés, politiques de rejet / fusion
- Livraison
- Clés d’idempotence, at-least-once + déduplication
- Présence
- Heartbeats, clés à TTL, reconnexion avec reprise
- État
- Acteur par connexion, magasin partagé pour la durabilité
Deux extraits de plus de la stack en usage.
L’ownership de Rust et les channels de Go résolvent le même problème — l’état partagé sous concurrence — depuis des extrémités opposées : l’un déplace l’ownership pour qu’il n’y ait rien à partager, l’autre partage en communiquant.
// Ownership moves; the compiler proves no use-after-move.
fn consume(buf: Vec<u8>) -> usize {
buf.len() // buf is owned here, freed at the end
}
fn main() {
let data = vec![1u8, 2, 3];
let n = consume(data);
// using 'data' here would not compile: it was moved.
println!('consumed {} bytes', n);
// Borrow instead of move when the caller keeps the value.
let kept = vec![4u8, 5];
let total: u8 = kept.iter().sum();
println!('{} still owns {} items', total, kept.len());
} // Share by communicating: a generator feeds a stage over a channel.
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
} Logique qui tourne près de l’utilisateur.
Un handler edge en TypeScript — du genre que je déploie sur Cloudflare Workers ou Vercel Edge. Il tourne au point de présence le plus proche de la requête, de sorte qu’une vérification de cache ou une porte d’auth ne paie jamais un aller-retour vers une région centrale.
// Runs at the edge: serve from cache, fall through to origin once.
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
const cache = caches.default;
const hit = await cache.match(req);
if (hit) return hit;
const res = await fetch(req);
if (res.ok && req.method === 'GET') {
const cached = new Response(res.body, res);
cached.headers.set('cache-control', 'public, max-age=60');
ctx.waitUntil(cache.put(req, cached.clone()));
return cached;
}
return res;
},
}; Le bon magasin pour le motif d’accès.
Je ne choisis pas un magasin de données par habitude. Je le choisis selon la forme des lectures et écritures qu’il doit absorber, en acceptant que la plupart des systèmes réels en utilisent plus d’un.
Un magasin relationnel gagne la position par défaut parce que la plupart des données sont relationnelles et que les transactions valent leur coût. Mais le chemin chaud qu’un cache sert, la question analytique qui scanne des milliards de lignes, et la persistance local-first sur un téléphone sont trois problèmes différents — et prétendre qu’un seul moteur répond aux trois, c’est ainsi que les systèmes deviennent lents de façons difficiles à défaire plus tard.
La discipline sous tous ces choix est la même : le schéma vit sous forme de migrations versionnées et réversibles dans le dépôt, les index sont conçus contre le plan de requête réel plutôt que devinés, et les requêtes du chemin chaud sont lues et réglées à la main même quand un ORM a écrit le premier jet.
PostgreSQL
Le magasin relationnel par défaut — transactions, JSONB là où il gagne sa place, et index conçus contre le plan de requête plutôt que devinés.
BigQuery
Échelle analytique. Stockage en colonnes et partitionnement pour les questions qui scannent des milliards de lignes mais s’exécutent rarement.
SQLite
Persistance embarquée et sur appareil pour mobile et edge — un seul fichier, zéro serveur, le bon outil pour les applications local-first.
Redis
Le chemin chaud — cache, limitation de débit, files, présence éphémère et verrous là où les microsecondes et les TTL comptent.
ORMs
Sequelize, Mongoose, GORM là où ils accélèrent la livraison — mais le SQL généré reste lisible, et les requêtes chaudes sont écrites à la main.
Migrations
Schéma sous forme de migrations versionnées et réversibles dans le dépôt, de sorte qu’une base de données soit reproductible plutôt qu’archéologique.
Le noyau sur lequel le conteneur tourne, traité comme partie du produit.
Une stack de défense en profondeur
La sécurité en couches, du boot signé jusqu’à l’audit hors hôte, pas une seule règle de pare-feu.
J’administre Linux jusqu’au noyau, et je traite l’hôte comme partie de la surface d’attaque plutôt que comme un endroit inerte où faire tourner un conteneur. La stack se lit du bas vers le haut : un boot signé et minimal et une base de sysctl, des utilisateurs à moindre privilège avec des clés SSH et sans mots de passe, un pare-feu default-deny, l’isolation des processus par sandboxing systemd et cgroups, et des logs d’audit expédiés hors de l’hôte pour qu’une compromission ne puisse pas effacer silencieusement sa propre trace.
Rien de tout cela n’est exotique — c’est la discipline ordinaire de faire tourner un hôte comme s’il allait être attaqué, parce qu’un jour il l’est. Le même instinct qui construit une petite CLI pour une friction récurrente construit une image de base durcie une fois et la réutilise partout, de sorte que le chemin sûr est aussi le chemin facile.
- Boot signé, modules de noyau minimaux, base de sysctl
- nftables default-deny, sandboxing systemd, seccomp, cgroups
- auditd et expédition des logs hors hôte ; mises à jour de sécurité non surveillées
Comment la stack s’est accumulée, couche par couche.
Aucune année à elle seule n’a tout enseigné. Cela s’est accumulé comme une stack le doit — de la vitre que l’utilisateur touche, vers le bas, jusqu’à ce que le noyau soit aussi familier que la vue.
- Années 1–3 Mobile natif, de bout en bout Swift et Objective-C sur iOS, Kotlin et Java sur Android. J’ai appris la discipline de livrer à des appareils sur le terrain — fragmentation, hors ligne, budgets de batterie et de mémoire.
- Années 3–6 Les backends derrière les applications Node, Spring, Django et Rails servant REST et GraphQL, soutenus par PostgreSQL et Redis. L’application a cessé de finir à l’API et a commencé au noyau.
- Années 6–9 Multiplateforme et cloud React Native et Flutter là où une base de code partagée payait ; conteneurs, Kubernetes et livraison multicloud pour que l’infrastructure devienne reproductible.
- Années 9–aujourd’hui Profondeur en systèmes et en distribué Rust et Go pour des cœurs qui doivent être corrects et rapides, des topologies orientées événements et temps réel, et une chaîne d’outils Linux personnelle et continue sous tout cela.
Open to the right work
Si vous avez besoin d’un seul ingénieur capable de tenir toute la stack — du noyau au cloud — c’est le travail que je fais.
If you are holding a problem that doesn't fit inside one field, that is the conversation I want.