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.

Le titre

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.

0+

années à livrer des applications mobiles à de vrais utilisateurs

0

plateformes mobiles en production : iOS, Android, Windows

0

clouds exploités sans verrouillage fournisseur : GCP, Azure, AWS

0

langages principaux maîtrisés en profondeur : Rust, Go, Python, TypeScript

La stack de langages

Une stack choisie par couche, pas par mode.

surface — utilisateurs, modèles, données TypeScript / JS Node, navigateur, edge. Python IA, données, calcul scientifique, automatisation. Go Services réseau concurrents. Rust Cœurs concurrents et sûrs en mémoire. C / C++ Proche du matériel. matériel — silicium, registres, firmware

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
Un petit morceau du réel

À 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
}
Quatre domaines, en profondeur

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
Topologie multicloud

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.

Cœur de l’app Docker · K8s · IaC GCP Cloud Run · GKE · Vertex AI Azure AKS · Functions · Apps AWS Lambda · EKS · S3 · RDS Edge CF Workers · Vercel Edge
01

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.

02

Azure

AKS, Functions, Cognitive Services, Container Apps — utilisé là où une organisation vit déjà dans l’écosystème Microsoft.

03

AWS

Lambda, ECS/EKS, S3, RDS, SQS/SNS derrière CloudFront — l’étendue de primitives pour les stacks de production traditionnels.

04

Edge

Cloudflare Workers et Vercel Edge — logique qui tourne près de l’utilisateur, pour la latence et la portée mondiale.

05

BaaS

Supabase et Firebase (Auth, Firestore, Realtime DB) — quand un produit a besoin d’un backend en jours, pas en semaines.

06

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.

L’instinct de celui qui construit ses outils
friction faite deux fois petit outil ~/.local/bin la chaîne d’outils se cumule

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
La stack, en un coup d’œil

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.

Architecture orientée événements

Découplé par message, pas par espoir.

producteur app · appareil producteur service broker Pub/Sub · file consommateur consommateur consommateur message mort

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
Pipeline de livraison

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
rollback — version précédente à une commande de distance commit build+ test analyse staging canarydéploiement prod
Services temps réel

État porté sur un socket, avec les modes de défaillance gérés.

client · ws client · ws client · ws passerelleterminer · acheminer handler handler magasin d’état partagé

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é
Un deuxième morceau du réel

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
}
Handler à l’edge

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;
  },
};
La couche de magasin de données

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.

01

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.

02

BigQuery

Échelle analytique. Stockage en colonnes et partitionnement pour les questions qui scannent des milliards de lignes mais s’exécutent rarement.

03

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.

04

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.

05

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.

06

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.

Stack de durcissement Linux

Le noyau sur lequel le conteneur tourne, traité comme partie du produit.

Mises à jour Correctifs de sécurité non surveillés, images reproductibles Audit auditd, rétention de journald, expédition des logs hors hôte Processus sandboxing systemd, seccomp, namespaces, cgroups Réseau nftables default-deny, fail2ban, ports segmentés Identité Utilisateurs à moindre privilège, politique sudo, clés SSH uniquement Boot et noyau Boot signé, modules minimaux, base de sysctl extérieur — surface exposée, surveillée et journalisée

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
Le parcours

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.

NextDefense Telecom