[
  {
    "id": "article:java-25-how-did-we-get-there:en",
    "href": "/blog/java-25-how-did-we-get-there",
    "locale": "en",
    "kind": "article",
    "title": "Java 25: How did we get there? (From OOP to DOP)",
    "description": "Discover the evolution of Java, from the object-oriented programming misunderstanding to the emergence of Data-Oriented Programming (DOP).",
    "tags": [
      "java",
      "oop",
      "fp",
      "dop",
      "brian-goetz"
    ],
    "content": "Java has radically changed. What was once the bastion of rigid Object-Oriented Programming (OOP) now embraces functional concepts to lead to a new paradigm: Data-Oriented Programming. A look back at three decades of history and mutations. Java's OOP: A Historical Misunderstanding? When Alan Kay created Smalltalk in the 70s, his vision of \"message passing\" between autonomous objects departed from what the term \"Object-Oriented Programming\" would later come to mean. For him, the essence lay in \"message passing\" [1]: a decentralized system of autonomous cells communicating with each other, more inspired by biological systems than a class hierarchy. Java, arriving in the 90s, took a more industrial direction: classes, encapsulation, mutable state, an explicit type hierarchy, and a portable platform. This choice was pragmatic and effective for the time, but it also created a lasting shift: we sometimes thought we were doing object-oriented programming in Alan Kay's sense, while we were mostly building complex object graphs where data and behavior were often too tightly linked to remain manageable. This gradual success wasn't so much due to the \"purity\" of this model as to the technical promise of the JVM: \"Write Once, Run Anywhere\". This success came with a certain architectural rigidity: the tendency to encapsulate all logic in classes, even for purely algorithmic treatments, even though static methods offered an alternative. While patterns like Service or Factory have clearly proven their utility for structuring large systems, their systematic use sometimes led to \"over-engineering\" where business logic disappears under layers of abstractions. We too often sought to solve state management difficulties through deep or fragile inheritance hierarchies, or through a multiplication of complex design patterns where a simple function would have sufficed. --- The Other Way: Lambda Calculus and Functional Programming (FP) In parallel with the rise of imperative programming, another school of thought developed on much older mathematical foundations: Alonzo Church's Lambda Calculus. Where OOP focuses on state and its mutation, functional programming (FP) treats software as an evaluation of pure functions. Based on immutability and referential transparency, this approach offers appreciable reasoning comfort: a function called with the same arguments will always produce the same result, without unexpected side effects. This philosophy radically transforms the development experience. It expresses itself well through the REPL (Read-Eval-Print Loop), a powerful tool inherited from Lisp and Scheme traditions, later adopted by languages like Clojure and Python. Unlike OOP where testing a method often requires instantiating a class and simulating a whole internal state (the \"tyranny of state\"), FP allows evaluating any fragment of logic instantly. This fluidity is naturally found in testing: testing a pure function is simpler (input -> output), reducing the need for complex mock frameworks and eliminating certain forms of boilerplate. Yet, despite these advantages, FP was long perceived as difficult to generalize. Not because the model is impractical: modern runtimes, persistent data structures, and optimizing compilers have reduced many of the costs. But computers still largely follow the Von Neumann model, where mutating memory is a natural and very efficient operation. In practice, pure functional programming therefore comes with trade-offs: additional allocation in some cases, recursion that must be handled carefully, performance models that are less intuitive for many developers, and an entry barrier around abstract mathematical concepts. While languages like Erlang (used in telecommunications), Clojure, and Scala showed the viability of FP, its mainstream adoption remains gradual, often through hybrid approaches rather than pure FP. --- Hybridization: The Best of Both Worlds Faced with the tension between dominant OOP and theoretical FP, a third way emerged: hybridization. Architects began to adopt the \"Functional Core, Imperative Shell\" pattern [3]: isolating business logic in a pure functional core and managing interactions with the external world in a thin imperative layer. This approach found concrete validation with React, showing that a \"flexible FP\" could simplify the construction of complex interfaces. Under the impetus of Brian Goetz, Java began this pragmatic mutation. Goetz, while admiring Clojure's clarity [2], refused to turn Java into a purely functional language. His approach was that of incremental evolution: identifying the most useful functional patterns — those that actually reduce errors — and integrating them idiomatically. This fundamental work, begun with Lambdas in Java 8, laid the groundwork for a more radical transformation: the transition to data-oriented programming. --- Data-Oriented Programming (DOP): The New Face of Java This is where Data-Oriented Programming (DOP) comes in [4], the practical fruition of this evolution. DOP prioritizes explicit and immutable data structures, rather than hiding them in object hierarchies. Java 21 and following versions, up to Java 25 LTS, instantiate this vision through three pillars that stabilized progressively: 1. Records: Finalized in Java 16, they are transparent, shallowly immutable data carriers that eliminate much of the boilerplate found in traditional data classes. 2. Sealed Types: Finalized in Java 17, they allow modeling closed hierarchies, precisely defining the possible alternatives for a piece of data. 3. Pattern Matching: Stabilized for switch and record patterns in Java 21, then enriched in later versions, it makes it possible to deconstruct and handle these structures precisely. Combined with Sealed Types, the compiler can detect unhandled cases. In concrete terms, modern Java DOP looks more like this than like a deep hierarchy of mutable classes: java sealed interface PaymentEvent permits PaymentAuthorized, PaymentCaptured, PaymentFailed {} record PaymentAuthorized(String paymentId, long amountInCents) implements PaymentEvent {} record PaymentCaptured(String paymentId) implements PaymentEvent {} record PaymentFailed(String paymentId, String reason) implements PaymentEvent {} String describe(PaymentEvent event) { return switch (event) { case PaymentAuthorized(var id, var amount) -> \"Payment \" + id + \" authorized for \" + amount; case PaymentCaptured(var id) -> \"Payment \" + id + \" captured\"; case PaymentFailed(var id, var reason) -> \"Payment \" + id + \" failed: \" + reason; }; } By combining these elements, Java allows modeling application domains less as actor hierarchies, more as explicit data structures. Rather than encapsulating data and behavior in the same class, we separate the structure of data from the logic that transforms it. This is a more direct approach and easier to reason about, combined with Java's type safety. --- Perspectives: Java as a Mainstream Language Influencer By becoming multi-paradigm, Java progressively redefines its positioning. Java, long criticized for its heaviness, now offers a balanced approach: the rigor of OOP for structuring systems, FP tools for clarifying logic, and DOP for representing data explicitly. Java maintains its dominant role in enterprise software while gaining recognition for these design choices. More than a quarter-century after its creation, Java continues to evolve, incorporating lessons from more recent paradigms while remaining pragmatic. --- References [1] Alan Kay, on the term \"Object-Oriented\": \"I'm sorry that I long ago coined the term 'objects' for this topic because it gets many people to focus on the lesser idea. The big idea is 'messaging'.\" (Source) [2] Brian Goetz, interview with Nicolai Parlog (2021): \"Clojure is the most interesting language on the JVM because it is materially different from Java.\" (Video) [3] Gary Bernhardt, \"Boundaries\" (RubyConf 2012): Talk introducing the Functional Core, Imperative Shell pattern. (Video) [4] Brian Goetz, \"Data-Oriented Programming in Java\" (InfoQ, 2022): Foundational article defining DOP principles applied to Java. (Article)"
  },
  {
    "id": "article:java-25-how-did-we-get-there:fr",
    "href": "/blog/java-25-how-did-we-get-there",
    "locale": "fr",
    "kind": "article",
    "title": "Java 25: Comment en est-on arrivé là ? (De OOP à DOP)",
    "description": "Découvrez l'évolution de Java, du malentendu de la programmation orientée objet vers l'émergence de la Data-Oriented Programming (DOP).",
    "tags": [
      "java",
      "oop",
      "fp",
      "dop",
      "brian-goetz"
    ],
    "content": "Java a radicalement changé. Ce qui était autrefois le bastion de la programmation orientée objet (OOP) rigide embrasse désormais les concepts fonctionnels pour aboutir à un nouveau paradigme : la Data-Oriented Programming. Retour sur trois décennies d'histoire et de mutations. L'OOP à la Java : un malentendu historique ? Lorsqu'Alan Kay a créé Smalltalk dans les années 70, sa vision du \"passage de messages\" entre objets autonomes s'écartait de l'interprétation ultérieure que le terme \"Object-Oriented Programming\" prendrait. Pour lui, l'essence résidait dans le \"passage de messages\" [1] : un système décentralisé de cellules autonomes communiquant entre elles, davantage inspiré par les systèmes biologiques qu'une hiérarchie de classes. Java, en arrivant dans les années 90, a pris une direction plus industrielle : des classes, de l'encapsulation, de l'état mutable, une hiérarchie de types explicite et une plateforme portable. Ce choix était pragmatique et efficace pour l'époque, mais il a aussi installé un glissement durable : nous pensions parfois faire de l'orienté objet au sens d'Alan Kay, alors que nous construisions surtout des graphes d'objets complexes, où la donnée et le comportement étaient souvent trop intimement liés pour rester gérables. Ce succès graduel ne tenait pas tant à la \"pureté\" de son modèle qu'à sa promesse technique : \"Write Once, Run Anywhere\". Cette réussite s'est accompagnée d'une certaine rigidité architecturale : la tendance à encapsuler toute logique dans des classes, même pour des traitements purement algorithmiques, même si les méthodes statiques offraient une alternative. Si les patterns de type Service ou Factory ont clairement prouvé leur utilité pour structurer de grands systèmes, leur utilisation systématique a parfois mené à une \"over-engineering\" où la logique métier disparaît sous des couches d'abstractions. On a trop souvent cherché à résoudre les difficultés de gestion de l'état par des hiérarchies d'héritage profondes ou par des design patterns complexes là où une simple fonction aurait suffi. --- L'autre voie : Le Lambda Calculus et la programmation fonctionnelle (FP) Parallèlement à l'essor de l'impératif, une autre école de pensée s'est développée sur des bases mathématiques bien plus anciennes : le Lambda Calculus d'Alonzo Church. Là où l'OOP se focalise sur l'état et sa mutation, la programmation fonctionnelle (FP) traite le logiciel comme une évaluation de fonctions pures. Reposant sur l'immutabilité et la transparence référentielle, cette approche offre un confort de raisonnement appréciable : une fonction appelée avec les mêmes arguments produira toujours le même résultat, sans effets de bord inattendus. Cette philosophie transforme radicalement l'expérience de développement. Elle s'exprime bien à travers le REPL (Read-Eval-Print Loop), un outil puissant hérité des traditions Lisp et Scheme, adopté ensuite par des langages comme Clojure et Python. Contrairement à l'OOP traditionnel où tester une méthode complexe peut exiger d'instancier une classe et de simuler un état interne, la FP permet d'évaluer instantanément n'importe quel fragment de logique. Cette fluidité se retrouve dans les tests : tester une fonction pure est plus simple (entrée -> sortie), réduisant le besoin de frameworks de mock complexes et éliminant certaines formes de boilerplate. Pourtant, malgré ces avantages, la FP a longtemps été perçue comme un luxe difficile à généraliser. La raison n'est pas que le modèle serait impraticable : des runtimes modernes, des structures de données persistantes et des compilateurs optimisants ont largement réduit les coûts. Mais nos ordinateurs restent proches du modèle de Von Neumann, où la mutation de la mémoire est une opération naturelle et très efficace. En pratique, le fonctionnel pur demande donc des compromis : allocation supplémentaire dans certains cas, récursion à encadrer, modèles de performance moins intuitifs pour beaucoup de développeurs, et une barrière d'entrée face à des concepts mathématiques abstraits. Si des langages comme Erlang (utilisé en télécommunications), Clojure ou Scala ont montré la viabilité de la FP, son adoption mainstream reste progressive, souvent à travers des approches hybrides plutôt que pure. --- L'Hybridation : Le meilleur des deux mondes ? Face à la tension entre l'OOP dominante et la FP théorique, une troisième voie a émergé : l'hybridation. Les architectes ont commencé à adopter le pattern \"Functional Core, Imperative Shell\" [3] : isoler la logique métier dans un noyau fonctionnel pur et gérer les interactions avec le monde extérieur dans une fine couche impérative. Cette approche a trouvé une validation concrète avec React, montrant qu'une \"FP souple\" pouvait simplifier la construction d'interfaces complexes. Sous l'impulsion de Brian Goetz, Java a entamé cette mutation pragmatique. Goetz, tout en admirant la clarté de Clojure [2], a refusé de transformer Java en un langage purement fonctionnel. Son approche a été celle de l'évolution incrémentale : identifier les patterns fonctionnels les plus utiles — ceux qui réduisent réellement le boilerplate et les erreurs — et les intégrer de manière idiomatique. Ce travail de fond, entamé avec les Lambdas en Java 8, a posé les jalons d'une transformation plus radicale : la transition vers une programmation orientée données. --- Data-Oriented Programming (DOP) : Le nouveau visage de Java C'est ici qu'intervient la Data-Oriented Programming (DOP) [4], l'aboutissement pratique de cette évolution. La DOP privilégie les structures de données explicites et immuables, plutôt que de les cacher dans des hiérarchies d'objets. Java 21 et les versions suivantes, jusqu'à Java 25 LTS, concrétisent cette vision via trois piliers qui se sont stabilisés progressivement : 1. Les Records : Finalisés en Java 16, ce sont des transporteurs de données transparents, shallowly immutable, qui éliminent une grande partie du boilerplate des classes de données. 2. Les Sealed Types : Finalisés en Java 17, ils permettent de modéliser des hiérarchies fermées, en définissant précisément les alternatives possibles d'une donnée. 3. Le Pattern Matching : Stabilisé pour switch et les record patterns en Java 21, puis enrichi dans les versions suivantes, il permet de déconstruire et traiter ces structures avec précision. Combiné aux Sealed Types, le compilateur peut détecter les cas non traités. Concrètement, une modélisation DOP en Java moderne ressemble davantage à cela qu'à une hiérarchie profonde de classes mutables : java sealed interface PaymentEvent permits PaymentAuthorized, PaymentCaptured, PaymentFailed {} record PaymentAuthorized(String paymentId, long amountInCents) implements PaymentEvent {} record PaymentCaptured(String paymentId) implements PaymentEvent {} record PaymentFailed(String paymentId, String reason) implements PaymentEvent {} String describe(PaymentEvent event) { return switch (event) { case PaymentAuthorized(var id, var amount) -> \"Payment \" + id + \" authorized for \" + amount; case PaymentCaptured(var id) -> \"Payment \" + id + \" captured\"; case PaymentFailed(var id, var reason) -> \"Payment \" + id + \" failed: \" + reason; }; } En combinant ces éléments, Java permet de modéliser les domaines applicatifs moins comme des hiérarchies d'acteurs, plus comme des structures de données explicites. Plutôt que d'encapsuler la donnée et le traitement dans une même classe, on sépare la structure des données de la logique qui les transforme. C'est une approche plus directe et plus facile à raisonner, combinée à la sécurité des types de Java. --- Perspectives : Java comme influenceur des langages mainstream En devenant multi-paradigme, Java redéfinit progressivement son positionnement. Java, longtemps critiqué pour sa lourdeur, offre désormais une approche équilibrée : la rigueur de l'OOP pour structurer les systèmes, les outils de la FP pour clarifier la logique, et la DOP pour représenter les données explicitement. Java garde son rôle prédominant dans l'entreprise tout en gagnant une reconnaissance pour ces choix de conception. Plus d'un quart de siècle après sa création, Java continue d'évoluer, prenant en compte les leçons de paradigmes plus récents tout en restant pragmatique. --- Références [1] Alan Kay, au sujet du terme \"Object-Oriented\" : \"I'm sorry that I long ago coined the term 'objects' for this topic because it gets many people to focus on the lesser idea. The big idea is 'messaging'.\" (Source) [2] Brian Goetz, interview avec Nicolai Parlog (2021) : \"Clojure is the most interesting language on the JVM because it is materially different from Java.\" (Vidéo) [3] Gary Bernhardt, \"Boundaries\" (RubyConf 2012) : Conférence introduisant le pattern Functional Core, Imperative Shell. (Vidéo) [4] Brian Goetz, \"Data-Oriented Programming in Java\" (InfoQ, 2022) : L'article fondateur qui définit les principes de la DOP appliqués à Java. (Article)"
  },
  {
    "id": "article:return-does-not-kill-your-goroutines:en",
    "href": "/blog/return-does-not-kill-your-goroutines",
    "locale": "en",
    "kind": "article",
    "title": "return doesn't kill your goroutines",
    "description": "Bounding the latency of a concurrent call in Go: a data race, then a permanent leak, then errgroup.",
    "tags": [
      "go",
      "goroutines",
      "errgroup",
      "concurrency",
      "context",
      "pprof"
    ],
    "content": "Bounding latency gives you a data race. Fixing it with channels, which Go has always recommended, turns a temporary leak into a permanent one. Detaching a context to silence context canceled errors removes the last safety net. Each decision is defensible on its own; together, they make an incident. Here is some code you have probably written before, and which runs in production at a lot of companies. A dashboard aggregates three calls — preferences, profile, orders — and since 1.2 seconds is too slow, we bound the latency. go var wg sync.WaitGroup wg.Go(func() { o, err := adapters.FetchOrders(ctx, userID) if err != nil { return } orders = o }) // ... same for profile and preferences done := make(chan struct{}) go func() { wg.Wait(); close(done) }() select { case <-done: return domain.Dashboard{Profile: profile, Preferences: prefs, Orders: orders}, nil case <-time.After(500 time.Millisecond): return domain.Dashboard{Profile: profile, Preferences: prefs, Orders: orders}, ErrBudgetExceeded } It's readable, go vet is happy, and it returns in 500 ms as requested (wg.Go has existed since Go 1.25; it's Add/Done without the ceremony). Let's add a few timestamped logs and see what really happens: preferences fails at 300 ms with a 503, profile answers at 900 ms, orders at 1.2 s. [ 301ms] preferences ✗ ERROR: preferences service unavailable (HTTP 503) [ 501ms] use case ⏱ giving up after 500ms — the goroutines carry on [ 901ms] profile ← written for a caller that is long gone [ 1220ms] orders ← written for a caller that is long gone The function returned at 501 ms. Four hundred milliseconds later, a goroutine writes to profile; seven hundred, another one writes to orders. They are dutifully filling in a dashboard that nobody will ever read. Go has no implicit join This is the sentence everything else follows from: a goroutine has no parent. Elsewhere, you get a handle — a Thread, a Future, a Task — that you can wait on, sometimes cancel, at the very least query. In Go, go f() returns nothing. No handle, no lineage, no lifetime tied to the calling function. So return kills nothing. It leaves the current function, without notifying or interrupting anyone. A WaitGroup knows how to wait — that is all it knows how to do — and the second you decide to stop waiting, you no longer have any hold on what you started. With a WaitGroup, you choose between blocking and leaking. There is no third option. The test that's green while the bug is right there These goroutines write to profile and orders — the very variables the select just read to build the return value. That is a data race, in the strict sense. go func TestTimeoutWritesAfterReturn(t testing.T) { , err := GetDashboardWithTimeout(context.Background(), \"1\") if !errors.Is(err, ErrBudgetExceeded) { t.Fatalf(\"expected the budget to be exceeded, got %v\", err) } time.Sleep(2 time.Second) // let the orphans write } $ go test ./internal/usecases/ ok bailleul.dev/articles-errgroup/internal/usecases 2.830s $ go test -race ./internal/usecases/ WARNING: DATA RACE Write at 0x00c0000941e0 by goroutine 9: usecases.GetDashboardWithTimeout.func2() internal/usecases/getdashboard1timeout.go:55 Previous read at 0x00c0000941e0 by goroutine 7: usecases.GetDashboardWithTimeout() internal/usecases/getdashboard1timeout.go:81 [...] --- FAIL: TestTimeoutWritesAfterReturn (2.50s) testing.go:1865: race detected during execution of test Two reports, one per surviving goroutine. Remember the contrast: the suite is green, the bug is there, only -race sees it. And this is not just \"a potentially stale value\". Go's memory model only bounds the damage for values that fit in a machine word. A slice, a string, an interface take several: the read can grab a pointer that doesn't match its length, and the crash will happen very far from the scene of the crime. The aside that cost me an hour While instrumenting this demo, I had added timestamped logs, protected by a mutex since several goroutines write to stdout. And -race went quiet. Zero races, while the bug was intact. The detector only reports accesses it cannot order, and a mutex creates order: it is a happens-before relationship. My goroutine took the logger's mutex right before writing to the shared variable; the caller had taken it a little earlier, around its read. That was all it took for the read/write pair to vanish from the report. My instrumentation had synchronized the bug I was trying to show. The fix: write first, log afterwards. The lesson goes beyond the demo: a silent -race does not prove the absence of a race. Your logs, your metrics, your traces — anything that takes a lock — can hide in tests a race that will show up in production, the day the debug log gets turned off. \"I'll fix it with a channel\" The problem is the shared variables. Let's replace them with channels; Go has been telling us since day one: don't communicate by sharing memory, share memory by communicating. go ordersCh := make(chan domain.Orders) go func() { o, err := adapters.FetchOrders(ctx, userID) if err != nil { return } ordersCh <- o }() deadline := time.After(500 time.Millisecond) for range 3 { select { case o := <-ordersCh: dash.Orders = o // ... profile, preferences case <-deadline: return dash, ErrBudgetExceeded } } It works: -race is clean, no more races. It is also strictly worse. The goroutines reach ordersCh <- o and never leave. The channel is unbuffered, the only receiver left at 500 ms, and a send with no receiver blocks forever. A blocked goroutine never returns, so it never frees anything — and in this demo, orders holds an 18 MB result. Before, the orphans finished their work and died: the leak was real but temporary. A temporary leak has just become permanent. By fixing the data race. At server scale One request leaking 18 MB never killed anybody. A server taking thousands of them is another story. Let's put these two versions behind an http.Handler — plus, as a sneak preview, the one this whole article is heading towards. Twenty sequential requests each, a fresh server every time, measured after a forced runtime.GC(): | version | right after | 3 s later | | ------------------- | --------------------- | ------------------------- | | hand-rolled timeout | 9 goroutines, 0 MB | 4 goroutines, 0 MB | | channels | 44 goroutines, 341 MB | 44 goroutines, 379 MB | | errgroup | 4 goroutines, 0 MB | 4 goroutines, 0 MB | The idle server is 4 goroutines: 44 − 4 = 40, that is two blocked goroutines per request, and 341 MB ≈ 20 × 18 MB. The first row surprised me: the hand-rolled timeout leak costs nothing in memory. Those goroutines die, so they drain as fast as they are created. If that were your only bug, your memory graphs would never alert you. The second one doesn't come back down — it keeps climbing, while the last goroutines finish their computation before freezing — and it never will: from the runtime's point of view, they are just waiting their turn. Where are they, exactly This is where net/http/pprof earns the import line it costs. $ curl -s 'localhost:8080/debug/pprof/goroutine?debug=1' 19 @ ... usecases.GetDashboardViaChannels.func2 getdashboard2channels.go:49 18 @ ... usecases.GetDashboardViaChannels.func3 getdashboard2channels.go:59 Line 59 is ordersCh <- o; line 49, the profile send. Dozens of copies of the same stack, all stopped on the same line: you've got your leak, no intuition required. Mount it on an internal port right now — the day the heap climbs, the question gets settled in ten seconds. The ingredient I had to add An honest aside: the first version of the server did not leak. Flat curve, with the faulty code nonetheless. net/http cancels r.Context() as soon as the handler returns, and that context went all the way down to the adapters, which listen to it. The goroutines were interrupted before they even reached their send. The platform was catching the bug for me. To reproduce the leak, I had to write this: go ctx := context.WithoutCancel(r.Context()) That line is everywhere. A team sees context canceled errors going by in its logs, concludes that cancellation is the problem rather than the symptom, and detaches the context. The errors disappear — and with them the only thing that was still telling the goroutines to stop. errgroup, at last All the leaks we've seen so far are symptoms of the same gap, and it was already there in the first snippet: wg.Go takes a func(), not a func() error. The goroutine that fails can only return. Nobody propagates the error, nobody warns the others. golang.org/x/sync/errgroup fills exactly that gap: go g, ctx := errgroup.WithContext(ctx) g.Go(func() error { o, err := adapters.FetchOrders(ctx, userID) if err != nil { return fmt.Errorf(\"fetch orders: %w\", err) } orders = o return nil }) // ... profile, preferences if err := g.Wait(); err != nil { return domain.Dashboard{}, err } Three things change, but the third one changes everything. The error propagates, wrapped, inspectable with errors.Is. The first error cancels the others — 300 ms instead of 1.2 s, including in the middle of a computation, if your loops check their context: the CPU also needs to be told to stop. And above all, no goroutine outlives the call. g.Wait() only returns once they have all finished: nothing to leak, and no race either, since Wait itself establishes the happens-before that was missing. That is structured concurrency: what goes into the function comes back out. Incidentally, the previous leak had a three-character fix: make(chan domain.Orders, 1), on each channel. The send no longer blocks, the goroutine dies, the memory goes away. It plugs the leak without addressing the swallowed error or the wasted work. It's a band-aid; errgroup is a treatment. The trap that cancels out the whole benefit go g, ctx := errgroup.WithContext(ctx) The original ctx is deliberately shadowed: it is the new one that must reach the outgoing calls. Keep the old one out of habit and all you have left is a WaitGroup with a nicer API. Worse, because more discreet: an adapter that doesn't listen to its context. Without a case <-ctx.Done(), the group can cancel all it wants, the goroutine sleeps in its call and will run to completion. Concretely: http.NewRequestWithContext and not http.NewRequest, QueryContext and not Query. Every blocking function without a context.Context is a hole in your cancellation. errgroup propagates a signal; it doesn't force anyone to listen to it. Three details before you dive in: g.Wait() only returns the first error; g.SetLimit(n) bounds concurrency in one line; and the group's ctx is canceled as soon as Wait() returns, even on success. What to take away return doesn't kill your goroutines, because in Go nothing kills them: they stop when they are done, or when they have been given a way to know they should stop. Look at the road travelled without ever writing an obviously stupid line. Bounding latency gives you a data race. Fixing it with channels, which Go has always recommended, turns a temporary leak into a permanent one. Detaching a context to silence context canceled errors removes the last safety net. Each decision is defensible on its own; together, they make an incident. errgroup fixes all of that at once: the error propagates, the siblings are canceled, nothing outlives the call. What the caller does with that error is its own business. Three reflexes, even if you remember nothing else: - Every blocking function takes a context.Context, and your compute loops check it. Cancellation only propagates where someone listens for it. - net/http/pprof on an internal port, right now. - A green -race proves nothing if your logging locks cross the suspect path. The repository contains the three versions, the server and the test: bash go test -race ./... # fails, on purpose go run ./cmd/server # then scripts/load-channels.sh"
  },
  {
    "id": "article:return-does-not-kill-your-goroutines:fr",
    "href": "/blog/return-does-not-kill-your-goroutines",
    "locale": "fr",
    "kind": "article",
    "title": "return ne tue pas vos goroutines",
    "description": "Borner la latence d'un appel concurrent en Go : une data race, puis une fuite définitive, puis errgroup.",
    "tags": [
      "go",
      "goroutines",
      "errgroup",
      "concurrency",
      "context",
      "pprof"
    ],
    "content": "Borner la latence donne une data race. La corriger avec des channels, ce que Go recommande depuis toujours, transforme une fuite temporaire en fuite définitive. Détacher un contexte pour faire taire des context canceled retire le dernier garde-fou. Chaque décision est défendable isolément ; ensemble, elles font un incident. Voici un code que vous avez sans doute déjà écrit, et qui tourne en production chez beaucoup de monde. Un dashboard agrège trois appels — préférences, profil, commandes — et comme 1,2 seconde c'est trop lent, on borne la latence. go var wg sync.WaitGroup wg.Go(func() { o, err := adapters.FetchOrders(ctx, userID) if err != nil { return } orders = o }) // ... idem pour profile et preferences done := make(chan struct{}) go func() { wg.Wait(); close(done) }() select { case <-done: return domain.Dashboard{Profile: profile, Preferences: prefs, Orders: orders}, nil case <-time.After(500 time.Millisecond): return domain.Dashboard{Profile: profile, Preferences: prefs, Orders: orders}, ErrBudgetExceeded } C'est lisible, go vet est content, et ça rend la main en 500 ms comme demandé (wg.Go existe depuis Go 1.25, c'est un Add/Done sans la cérémonie). Ajoutons quelques logs horodatés et voyons ce qui se passe vraiment : preferences tombe à 300 ms avec un 503, profile répond à 900 ms, orders à 1,2 s. [ 301ms] preferences ✗ ERROR: preferences service unavailable (HTTP 503) [ 501ms] use case ⏱ giving up after 500ms — the goroutines carry on [ 901ms] profile ← written for a caller that is long gone [ 1220ms] orders ← written for a caller that is long gone La fonction a rendu la main à 501 ms. Quatre cents millisecondes plus tard, une goroutine écrit dans profile ; sept cents, une autre écrit dans orders. Elles remplissent consciencieusement un dashboard que plus personne ne lira. Go n'a pas de join implicite C'est la phrase dont tout le reste découle : une goroutine n'a pas de parent. Ailleurs, vous avez un handle — un Thread, une Future, une Task — que vous pouvez attendre, parfois annuler, au minimum interroger. En Go, go f() ne rend rien. Pas de handle, pas de filiation, pas de durée de vie adossée à la fonction appelante. Donc return ne tue rien. Il sort de la fonction courante, sans notifier ni interrompre personne. La WaitGroup sait attendre — c'est tout ce qu'elle sait faire — et à la seconde où vous décidez de ne plus attendre, vous n'avez plus aucune prise sur ce que vous avez lancé. Avec une WaitGroup, vous choisissez entre bloquer et fuir. Il n'y a pas de troisième option. Le test qui est vert alors que le bug est là Ces goroutines écrivent dans profile et orders — les variables que le select vient de lire pour construire la valeur de retour. C'est une data race, au sens strict. go func TestTimeoutWritesAfterReturn(t testing.T) { , err := GetDashboardWithTimeout(context.Background(), \"1\") if !errors.Is(err, ErrBudgetExceeded) { t.Fatalf(\"expected the budget to be exceeded, got %v\", err) } time.Sleep(2 time.Second) // let the orphans write } $ go test ./internal/usecases/ ok bailleul.dev/articles-errgroup/internal/usecases 2.830s $ go test -race ./internal/usecases/ WARNING: DATA RACE Write at 0x00c0000941e0 by goroutine 9: usecases.GetDashboardWithTimeout.func2() internal/usecases/getdashboard1timeout.go:55 Previous read at 0x00c0000941e0 by goroutine 7: usecases.GetDashboardWithTimeout() internal/usecases/getdashboard1timeout.go:81 [...] --- FAIL: TestTimeoutWritesAfterReturn (2.50s) testing.go:1865: race detected during execution of test Deux rapports, un par goroutine survivante. Retenez le contraste : la suite est verte, le bug est là, seul -race le voit. Et ce n'est pas juste « une valeur potentiellement périmée ». Le modèle mémoire de Go ne borne les dégâts que pour les valeurs d'un mot machine. Une slice, une string, une interface en font plusieurs : la lecture peut attraper un pointeur qui ne va pas avec sa longueur, et le crash aura lieu très loin du lieu du crime. L'aparté qui m'a coûté une heure En instrumentant cette démo, j'avais ajouté des logs horodatés, protégés par un mutex puisque plusieurs goroutines écrivent sur stdout. Et -race s'est tu. Zéro course, alors que le bug était intact. Le détecteur ne signale que les accès qu'il ne sait pas ordonner, et un mutex crée de l'ordre : c'est une relation happens-before. Ma goroutine prenait celui du logger juste avant d'écrire dans la variable partagée ; l'appelant l'avait pris un peu plus tôt, autour de sa lecture. Il n'en fallait pas plus pour que la paire lecture/écriture disparaisse du rapport. Mon instrumentation avait synchronisé le bug que j'essayais de montrer. Le correctif : écrire d'abord, loguer ensuite. La leçon dépasse la démo : un -race silencieux ne prouve pas l'absence de course. Vos logs, vos métriques, vos traces — tout ce qui prend un verrou — peuvent masquer en test une course qui s'exprimera en production, le jour où le log de debug sera désactivé. « Je vais corriger ça avec un channel » Le problème, ce sont les variables partagées. Remplaçons-les par des channels, Go nous le répète depuis ses débuts : ne communiquez pas en partageant la mémoire, partagez la mémoire en communiquant. go ordersCh := make(chan domain.Orders) go func() { o, err := adapters.FetchOrders(ctx, userID) if err != nil { return } ordersCh <- o }() deadline := time.After(500 time.Millisecond) for range 3 { select { case o := <-ordersCh: dash.Orders = o // ... profile, preferences case <-deadline: return dash, ErrBudgetExceeded } } Ça marche : -race est propre, plus aucune course. C'est aussi strictement pire. Les goroutines arrivent au ordersCh <- o et n'en repartent jamais. Le channel n'est pas bufferisé, l'unique récepteur est parti à 500 ms, et un envoi sans récepteur bloque pour toujours. Une goroutine bloquée ne retourne jamais, donc ne libère jamais rien — et dans cette démo, orders tient un résultat de 18 Mo. Avant, les orphelines finissaient leur travail et mouraient : la fuite était réelle mais temporaire. Une fuite temporaire vient de devenir définitive. En corrigeant la data race. À l'échelle d'un serveur Une requête qui fuit 18 Mo, personne n'en meurt. Un serveur qui en encaisse des milliers, si. Mettons ces deux versions derrière un http.Handler — plus, en avant-première, celle vers laquelle tout cet article se dirige. Vingt requêtes séquentielles chacune, serveur neuf à chaque fois, relevé après un runtime.GC() forcé : | version | juste après | 3 s plus tard | | -------------- | --------------------- | ------------------------- | | timeout maison | 9 goroutines, 0 Mo | 4 goroutines, 0 Mo | | channels | 44 goroutines, 341 Mo | 44 goroutines, 379 Mo | | errgroup | 4 goroutines, 0 Mo | 4 goroutines, 0 Mo | Le serveur au repos, c'est 4 goroutines : 44 − 4 = 40, soit deux goroutines bloquées par requête, et 341 Mo ≈ 20 × 18 Mo. La première ligne m'a surpris : la fuite du timeout maison ne coûte rien en mémoire. Ces goroutines meurent, donc elles se résorbent aussi vite qu'elles se créent. Si vous n'aviez que ce bug-là, vos graphes mémoire ne vous alerteraient jamais. La deuxième ne redescend pas — elle monte encore, le temps que les dernières goroutines finissent leur calcul avant de se figer — et ne redescendra plus : du point de vue du runtime, elles attendent juste leur tour. Où sont-elles, exactement C'est là que net/http/pprof justifie la ligne d'import qu'il coûte. $ curl -s 'localhost:8080/debug/pprof/goroutine?debug=1' 19 @ ... usecases.GetDashboardViaChannels.func2 getdashboard2channels.go:49 18 @ ... usecases.GetDashboardViaChannels.func3 getdashboard2channels.go:59 La ligne 59, c'est ordersCh <- o ; la 49, l'envoi de profile. Des dizaines d'exemplaires d'une même stack, toutes arrêtées sur la même ligne : vous tenez votre fuite, sans avoir eu besoin d'intuition. Montez-le sur un port interne dès maintenant — le jour où le heap grimpe, la question se règle en dix secondes. L'ingrédient qu'il a fallu ajouter Parenthèse honnête : la première version du serveur ne fuyait pas. Courbe plate, avec le code pourtant fautif. net/http annule r.Context() dès que le handler retourne, et ce contexte descendait jusqu'aux adapters, qui l'écoutent. Les goroutines étaient interrompues avant même d'atteindre leur envoi. La plateforme rattrapait le bug à ma place. Pour reproduire la fuite, il a fallu écrire ceci : go ctx := context.WithoutCancel(r.Context()) Cette ligne, on la trouve partout. Une équipe voit passer des context canceled dans ses logs, conclut que l'annulation est le problème plutôt que le symptôme, et détache le contexte. Les erreurs disparaissent — et avec elles la seule chose qui disait encore aux goroutines de s'arrêter. errgroup, enfin Toutes les fuites vues jusqu'ici sont des symptômes du même manque, et il était déjà là dans le premier extrait : wg.Go prend un func(), pas un func() error. La goroutine qui échoue ne peut que faire return. Personne ne remonte l'erreur, personne ne prévient les autres. golang.org/x/sync/errgroup comble exactement ça : go g, ctx := errgroup.WithContext(ctx) g.Go(func() error { o, err := adapters.FetchOrders(ctx, userID) if err != nil { return fmt.Errorf(\"fetch orders: %w\", err) } orders = o return nil }) // ... profile, preferences if err := g.Wait(); err != nil { return domain.Dashboard{}, err } Trois choses changent, mais la troisième change tout. L'erreur remonte, enveloppée, inspectable avec errors.Is. La première erreur annule les autres — 300 ms au lieu de 1,2 s, y compris au milieu d'un calcul, si vos boucles regardent leur contexte : le CPU aussi a besoin qu'on lui dise d'arrêter. Et surtout, aucune goroutine ne survit à l'appel. g.Wait() ne rend la main que lorsqu'elles sont toutes terminées : rien à fuir, et pas de course non plus, puisque le Wait établit lui-même le happens-before qui manquait. C'est ça, la concurrence structurée : ce qui entre dans la fonction en ressort. Au passage, la fuite précédente avait un correctif de trois caractères : make(chan domain.Orders, 1), sur chaque channel. L'envoi ne bloque plus, la goroutine meurt, la mémoire part. Ça bouche la fuite sans régler l'erreur avalée ni le travail gaspillé. C'est un pansement ; errgroup est un traitement. Le piège qui annule tout le bénéfice go g, ctx := errgroup.WithContext(ctx) Le ctx d'origine est volontairement masqué (shadowing) : c'est le nouveau qui doit arriver jusqu'aux appels sortants. Gardez l'ancien par habitude et vous n'avez plus qu'une WaitGroup avec une plus jolie API. Pire parce que plus discret : un adapter qui n'écoute pas son contexte. Sans case <-ctx.Done(), le groupe a beau annuler, la goroutine dort dans son appel et ira au bout. Concrètement : http.NewRequestWithContext et pas http.NewRequest, QueryContext et pas Query. Toute fonction bloquante sans context.Context est un trou dans votre annulation. errgroup propage un signal ; il ne force personne à l'écouter. Trois détails avant de vous lancer : g.Wait() ne rend que la première erreur ; g.SetLimit(n) borne la concurrence en une ligne ; et le ctx du groupe est annulé dès le retour de Wait(), même en cas de succès. Ce qu'il faut retenir return ne tue pas vos goroutines, parce qu'en Go rien ne les tue : elles s'arrêtent quand elles ont fini, ou quand on leur a donné le moyen de savoir qu'il faut arrêter. Regardez le chemin parcouru sans jamais écrire une ligne manifestement stupide. Borner la latence donne une data race. La corriger avec des channels, ce que Go recommande depuis toujours, transforme une fuite temporaire en fuite définitive. Détacher un contexte pour faire taire des context canceled retire le dernier garde-fou. Chaque décision est défendable isolément ; ensemble, elles font un incident. errgroup répare tout ça d'un coup : l'erreur remonte, les voisines sont annulées, rien ne survit à l'appel. Ce que l'appelant fait de cette erreur, c'est son affaire. Trois réflexes, même si vous ne retenez rien d'autre : - Toute fonction bloquante prend un context.Context, et vos boucles de calcul le regardent. L'annulation ne se propage qu'où on l'écoute. - net/http/pprof sur un port interne, dès maintenant. - Un -race vert ne prouve rien si vos verrous de log traversent le chemin suspect. Le dépôt contient les trois versions, le serveur et le test : bash go test -race ./... # échoue, c'est voulu go run ./cmd/server # puis scripts/load-channels.sh"
  },
  {
    "id": "work:adeo-multibu-data-platform:en",
    "href": "/work/adeo-multibu-data-platform",
    "locale": "en",
    "kind": "work",
    "title": "ADEO – Multi-Business-Unit Data Platform",
    "description": "Designed a strategic platform enabling business units to define their data schemas and share only explicitly authorized information.",
    "tags": [
      "Node.js",
      "MongoDB",
      "API",
      "Authorization"
    ],
    "content": "Delivered the MVP of an internal multi-business-unit platform with configurable schemas, integrity controls, and read authorization between entities. ADEO Agence Dernier Cri Strategic MVP delivered on schedule by a multi-vendor project team Context ADEO launched a strategic internal project serving multiple business units. A dedicated team bringing together contractors from several companies was assembled to deliver the MVP. Need Each business unit needed to define its own data schemas through an API. The platform had to validate the consistency and integrity of stored information while allowing data to be shared between entities under explicit authorization. Implementation I contributed to the design and development of the platform with Node.js and MongoDB, including: - configurable schema definition through the API; - validation, consistency, and integrity controls; - an inter-business-unit authorization system restricting read access to explicitly granted data. Outcome The MVP of this strategic project was delivered on schedule by the team assembled to build it."
  },
  {
    "id": "work:adeo-multibu-data-platform:fr",
    "href": "/work/adeo-multibu-data-platform",
    "locale": "fr",
    "kind": "work",
    "title": "ADEO – Plateforme de données multi-BU",
    "description": "Conception d'une plateforme stratégique permettant aux business units de définir leurs schémas de données et de partager uniquement les informations autorisées.",
    "tags": [
      "Node.js",
      "MongoDB",
      "API",
      "Autorisations"
    ],
    "content": "Réalisation du MVP d'une plateforme interne multi-BU avec schémas configurables, contrôles d'intégrité et autorisations de lecture entre entités. ADEO Agence Dernier Cri MVP stratégique livré dans les délais par une équipe projet multi-prestataires Contexte ADEO lançait un projet stratégique interne destiné à plusieurs business units. Une équipe spécifique réunissant des prestataires issus de plusieurs entreprises a été constituée pour réaliser le MVP. Besoin Chaque business unit devait pouvoir définir ses propres schémas de données au travers d'une API. La plateforme devait contrôler la validité et l'intégrité des informations enregistrées, tout en permettant leur partage entre entités selon des autorisations explicites. Réalisation J'ai contribué à la conception et au développement de la plateforme avec Node.js et MongoDB, notamment sur : - la définition de schémas configurables via l'API ; - les contrôles de validation, de cohérence et d'intégrité ; - le système d'autorisations inter-BU limitant la lecture aux seules données accordées. Résultat Le MVP de ce projet stratégique a été livré dans les délais par l'équipe constituée pour le réaliser."
  },
  {
    "id": "work:mailjet-sendapi-v31:en",
    "href": "/work/mailjet-sendapi-v31",
    "locale": "en",
    "kind": "work",
    "title": "Mailjet SendAPI v3.1 – Distributed Go API",
    "description": "Architected and implemented a Go email delivery API capable of handling millions of requests.",
    "tags": [
      "Go",
      "API",
      "Distributed Systems",
      "GCP",
      "Cassandra"
    ],
    "content": "Designed and delivered Mailjet SendAPI v3.1 on an existing GCP infrastructure, with Cassandra storage integrated with historical systems. Mailjet Agence Dernier Cri Distributed API delivered to handle millions of email requests Context Mailjet needed to evolve its email delivery API while integrating it with its existing infrastructure and historical systems. Implementation I contributed to the architecture and implementation of SendAPI v3.1 in Go. The distributed API was delivered to the existing Google Cloud Platform infrastructure. The system combined: - services developed in Go; - Cassandra message storage; - integration with legacy Free Pascal components; - containerized delivery with Docker. Outcome The new API was designed and delivered to handle millions of email delivery requests."
  },
  {
    "id": "work:mailjet-sendapi-v31:fr",
    "href": "/work/mailjet-sendapi-v31",
    "locale": "fr",
    "kind": "work",
    "title": "Mailjet SendAPI v3.1 – API distribuée en Go",
    "description": "Architecture et implémentation en Go d'une API d'envoi d'emails capable de traiter des millions de requêtes.",
    "tags": [
      "Go",
      "API",
      "Systèmes distribués",
      "GCP",
      "Cassandra"
    ],
    "content": "Conception et livraison de Mailjet SendAPI v3.1 sur une infrastructure GCP existante, avec un stockage Cassandra relié aux systèmes historiques. Mailjet Agence Dernier Cri API distribuée livrée pour traiter des millions de requêtes d'envoi Contexte Mailjet devait faire évoluer son API d'envoi d'emails tout en l'intégrant à son infrastructure et à ses systèmes historiques. Réalisation J'ai participé à l'architecture et à l'implémentation de SendAPI v3.1 en Go. L'API distribuée a été livrée sur l'infrastructure Google Cloud Platform existante. Le système combinait : - des services développés en Go ; - un stockage de messages dans Cassandra ; - une intégration avec des composants historiques en Free Pascal ; - un déploiement conteneurisé avec Docker. Résultat La nouvelle API a été conçue et livrée pour traiter des millions de requêtes d'envoi d'emails."
  },
  {
    "id": "work:pagily-document-platform:en",
    "href": "/work/pagily-document-platform",
    "locale": "en",
    "kind": "work",
    "title": "Pagily – Web and Mobile Document Platform",
    "description": "Led the technical direction and delivery of a document management platform spanning backend services, web, mobile, and scanning equipment.",
    "tags": [
      "React",
      "Next.js",
      "React Native",
      ".NET"
    ],
    "content": "End-to-end design of a paperless, universal-scanning, and secure-printing solution, from .NET services to React and React Native products. Pagily Independent End-to-end architecture and delivery across backend services, web, mobile, and document processing Context Pagily is a document management platform dedicated to paperless workflows, universal scanning, and secure printing. Responsibility As an independent software engineer, I led the technical direction, architecture decisions, and product delivery. Implementation The platform brought together several components: - .NET/C# backend services for secure document processing and indexing; - a web application developed with React and Next.js; - a React Native mobile application; - a pipeline automating document classification and indexing; - integrations with multifunction printers and scanning equipment. Outcome The product was designed and delivered end to end, from document services to web and mobile experiences and hardware integrations."
  },
  {
    "id": "work:pagily-document-platform:fr",
    "href": "/work/pagily-document-platform",
    "locale": "fr",
    "kind": "work",
    "title": "Pagily – Plateforme documentaire web et mobile",
    "description": "Pilotage technique et livraison d'une plateforme de GED couvrant les services documentaires, le web, le mobile et les équipements de numérisation.",
    "tags": [
      "React",
      "Next.js",
      "React Native",
      ".NET"
    ],
    "content": "Conception de bout en bout d'une solution de dématérialisation, de scan universel et d'impression sécurisée, des services .NET aux produits React et React Native. Pagily Indépendant Architecture et livraison de bout en bout des services backend, du web, du mobile et des traitements documentaires Contexte Pagily est une plateforme de gestion électronique de documents dédiée à la dématérialisation, au scan universel et à l'impression sécurisée. Responsabilité En tant qu'ingénieur logiciel indépendant, j'ai piloté la direction technique, les choix d'architecture et la livraison du produit. Réalisation La plateforme réunissait plusieurs composantes : - des services backend en .NET/C# pour le traitement et l'indexation sécurisée des documents ; - une application web développée avec React et Next.js ; - une application mobile React Native ; - une chaîne automatisant la classification et l'indexation documentaire ; - des intégrations avec des imprimantes multifonctions et des équipements de scan. Résultat Le produit a été conçu et livré de bout en bout, depuis les services documentaires jusqu'aux expériences web et mobile et aux intégrations matérielles."
  },
  {
    "id": "work:silae-payroll-support-ai:en",
    "href": "/work/silae-payroll-support-ai",
    "locale": "en",
    "kind": "work",
    "title": "Silae HR Suite – Applied AI for Payroll Support",
    "description": "Designed an agentic workflow to explain payroll items and accelerate the handling of customer support requests.",
    "tags": [
      "Python",
      "FastAPI",
      "LangGraph",
      "MCP",
      "Generative AI",
      "HR"
    ],
    "content": "A case study on integrating a Python/FastAPI agentic workflow into an existing HR product, supported by stronger engineering safeguards and team enablement. Silae Andjaro / Silae 80% reduction in the backlog of requests for payroll-item explanations Context Silae HR Suite is an HRIS SaaS product for small and mid-sized companies. It covers HR processes from hiring to payroll preparation and is used by more than 2,000 customers to manage approximately 230,000 employment contracts per year. Within the product team, a recurring share of customer support requests concerned explanations of payroll items. Implementation I designed a Python and FastAPI agentic workflow based on LangGraph to produce these explanations and accelerate their handling. The work also addressed the reliability of AI-assisted development through: - stronger tests; - Claude policies; - reusable skills; - Model Context Protocol (MCP) integration; - subagents used to structure tasks. Adoption and Outcome I supported six team members with no prior Claude experience through adoption of these practices despite strong initial reluctance. The workflow reduced the part of the support backlog related to payroll-item explanation requests by 80%."
  },
  {
    "id": "work:silae-payroll-support-ai:fr",
    "href": "/work/silae-payroll-support-ai",
    "locale": "fr",
    "kind": "work",
    "title": "Silae RH Suite – IA appliquée au support paie",
    "description": "Conception d'un workflow agentique pour expliquer les éléments de paie et accélérer le traitement des demandes par le support client.",
    "tags": [
      "Python",
      "FastAPI",
      "LangGraph",
      "MCP",
      "IA générative",
      "RH"
    ],
    "content": "Retour sur l'intégration d'un workflow agentique Python/FastAPI dans un produit RH existant, avec un cadre de développement renforcé et un accompagnement de l'équipe. Silae Andjaro / Silae 80 % de backlog en moins sur les demandes d'explication des éléments de paie Contexte Silae RH Suite est un SIRH SaaS destiné aux PME et ETI. La solution couvre les processus RH de l'embauche à la préparation de la paie et est utilisée par plus de 2 000 clients pour gérer environ 230 000 contrats de travail par an. Au sein de l'équipe produit, une partie récurrente des demandes adressées au support concernait l'explication des éléments de paie. Réalisation J'ai conçu en Python et FastAPI un workflow agentique fondé sur LangGraph afin de produire ces explications et d'accélérer leur traitement. Le travail a également porté sur la fiabilité du développement assisté par IA : - renforcement des tests ; - définition de politiques Claude ; - création de skills réutilisables ; - intégration du Model Context Protocol (MCP) ; - utilisation de sous-agents pour structurer les tâches. Adoption et résultat J'ai accompagné six membres de l'équipe, sans expérience préalable de Claude, jusqu'à l'adoption de ces pratiques malgré une forte réticence initiale. Le workflow a réduit de 80 % la partie du backlog support liée aux demandes d'explication des éléments de paie."
  },
  {
    "id": "homelab:greybound-agentic-audio:en",
    "href": "/homelab/greybound-agentic-audio",
    "locale": "en",
    "kind": "homelab",
    "title": "Greybound – Building a virtual amplifier with agents",
    "description": "A fully agentic experiment: turn the Rust compiler's guarantees into a working framework for AI, then evaluate audio models autonomously in Python.",
    "tags": [
      "Rust",
      "Python",
      "DSP",
      "MCP",
      "AI Agents"
    ],
    "content": "Rust remains an intimidating language to me. Greybound explores whether its compilation constraints can become an advantage for AI: agents turn versioned specifications into DSP models, render audio and evaluate candidates automatically in Python against SPICE and NAM. Greybound Spec-driven development · Autonomous Python evaluation · Real-time Rust audio engine Greybound: a virtual guitar amplifier with control knobs and a crosshatch grille, next to a scope card comparing a candidate render against a reference waveform in an automated agent evaluation loop Starting with a language that intimidates me Rust is a language that intimidates me. Its ownership model, type system and memory constraints demand a level of rigour that I do not claim to have mastered in every detail. That is precisely where Greybound starts. My hypothesis is that what makes Rust demanding for a developer can become an advantage for AI. The compiler gives agents a deterministic, precise feedback loop that is difficult to evade. A lifetime error, unsafe state sharing or a type mismatch cannot be hidden behind an implementation that merely looks plausible: the program must satisfy the contract before it can even be evaluated. The real purpose of the experiment is therefore this: to test how far agents can autonomously develop a complex technical system when the language, specifications and evaluation tools impose strong guarantees. Audio makes the experiment particularly useful. Successful compilation cannot prove that a virtual amplifier sounds or reacts correctly. Rust closes the first loop around program validity; a Python lab closes a second loop around the quality of the resulting models. Modelling an amplifier, not merely producing a sound Greybound is the concrete vehicle for this experiment: a real-time audio engine for guitar amplifiers and pedals. Its core is written in Rust and runs as a standalone application or a CLAP/VST3 plugin. The Nox30 model, for example, follows the structure of a British valve amplifier: ECC83 stages, tone stack, phase inverter, an EL84 quartet, a sagging supply and an output transformer. The goal is neither to reproduce every component inside a general-purpose circuit simulator nor to capture the entire amplifier in a black box. Greybound uses a gray-box approach: circuit topology remains explicit, while expensive or uncertain cells can be approximated, fitted or learned offline. The final engine must remain deterministic, inspectable and suitable for real-time operation. Greybound therefore becomes the test bed for fully agentic development, driven by specifications and closed by an objective evaluation loop. Autonomy starts with a specification An agentic loop only becomes genuinely autonomous when the agent knows what it may change, how to verify its work and when it must reject it. In Greybound, the repository itself provides that contract. A general policy defines architecture and validation invariants. A versioned knowledge base then describes: - the engine, routing and lab architecture; - the topology and confidence level of every amplifier or pedal; - reusable circuit cells, their units and assumptions; - the current project state, decisions and open questions; - the protocol, inputs and decision criteria for every experiment. JSON5 rig files contain musical topology and controls only. Runtime choices — sample rate, block size, input file, duration, reference and output — stay outside them. This separation lets the agent generate temporary variants without silently changing the reference configuration. The specification is not a document written once and forgotten. It is the project's operational memory. When an experiment disproves a hypothesis, its result and observed limitation join that memory before the next iteration. A closed development loop Given a bounded objective, the agent can execute the whole campaign autonomously: text specification and hypothesis ↓ smallest useful Rust DSP change ↓ tests, build and deterministic WAV render ↓ Python evaluation against SPICE, NAM or a baseline ↓ report, decision and next hypothesis └──────────────────────────────↺ Every render retains its provenance: Git revision, exact command, source rig, temporary control overrides, audio input, impulse response, sample rate and generated artifacts. The monitor records xruns, silence, RMS and peak levels, clipping and internal quantities such as supply voltage, currents and transformer flux. If a render is unstable or clips numerically, the experiment stops before any tonal comparison. An invalid output cannot become a “better model” through a flattering score. Python as the measurement bench Rust remains responsible for the real-time audio path. Python is used offline, where NumPy, SciPy, ngspice and PyTorch provide a scientific workbench. The lab first aligns a candidate with its reference in time and gain. It then measures several families of behaviour: spectral distance, null residual, dynamic envelope, attacks, level response, harmonics, intermodulation, noise, aliasing, phase, decay and sag recovery. Synthetic stimuli isolate phenomena that are difficult to identify in a guitar phrase: level-stepped sines, two-tone tests, short attacks, high-frequency stress and power-supply bursts. References change with the scale being observed: - SPICE is the electrical oracle for a particular cell or node; - NAM supplies a realistic external reference for the complete audio chain; - a previous Greybound render acts as the baseline when no stronger reference exists. Metrics are never reduced to one universal truth. A composite score helps rank a campaign, but a spectral improvement may hide a weaker attack, flattened dynamics or instability. The agent must inspect metric families, state trade-offs and keep the test conditions fixed. Giving the instruments directly to agents Greybound Lab exposes this measurement chain through MCP. An agent can inspect a rig, list available inputs and references, apply temporary controls, launch a bounded render, analyse its health, compare two WAV files and summarise the generated report. The same experiments are visible in a human-facing lab application. Both interfaces share the same versioned artifact, greybound.lab.run.v1. An experiment launched by an agent can therefore be replayed and inspected in the UI, while a render imported by a human can enter the agentic loop. This surface is deliberately constrained. Renders have a maximum duration, variants do not overwrite source rigs and external plugin adapters are declarative. Autonomy applies to experimentation, not arbitrary command execution. An agent must also know how to lose The most important result of an autonomous loop is not that it produces more code. It is that it can reject its own solution. One experiment trained a small static network in Python to reproduce a SPICE-simulated 12AX7 stage. It reached a weighted root mean square error of 49.555 mV. Adding eight input-history samples looked like the natural next step, yet the result regressed to 54.698 mV. The candidate was discarded instead of being retained merely because it was more sophisticated. The following iteration returned to a more explicit gray-box structure with fast and slow physical states and a bounded nonlinearity. It reached 35.207 mV, compared with 95.318 mV for the analytic cell under the same conditions. But that local success was not sufficient: once inserted into the complete amplifier, the NAM-facing improvement remained small and another indicator became slightly worse. The model stayed an integration probe until further validation justified freezing a real-time variant. That ability to try, measure, contradict and change direction is what turns AI code generation into an agentic engineering process. The human role moves up one level In this organisation, I do not intervene in every micro-iteration. I define the target, the credibility of references, real-time constraints and promotion rules. The agent then owns project-state discovery, implementation, experiments, analysis and documentation until it reaches a stopping criterion or exposes an uncertainty that requires a decision. Listening and playing guitar remain useful, but they become product validation at selected milestones. They do not replace measurement and are not required to supervise every intermediate candidate. Greybound interests me as much for its development process as for the sound it produces. It shows that an agent can work autonomously for extended periods on a difficult technical problem, provided it has structured memory, bounded tools and a way to prove that a change is actually better. Link - Greybound source code"
  },
  {
    "id": "homelab:greybound-agentic-audio:fr",
    "href": "/homelab/greybound-agentic-audio",
    "locale": "fr",
    "kind": "homelab",
    "title": "Greybound – Développer un ampli virtuel avec des agents",
    "description": "Une expérimentation full agentic : faire des garanties du compilateur Rust un cadre de travail pour l'IA, puis évaluer les modèles audio de façon autonome en Python.",
    "tags": [
      "Rust",
      "Python",
      "DSP",
      "MCP",
      "Agents IA"
    ],
    "content": "Rust reste un langage intimidant pour moi. Greybound explore l'hypothèse que ses contraintes de compilation peuvent devenir un atout pour l'IA : des agents transforment des spécifications versionnées en modèles DSP, les rendent en audio et les évaluent automatiquement en Python face à SPICE et NAM. Greybound Développement spec-driven · Évaluation autonome en Python · Moteur audio temps réel en Rust Greybound : un ampli guitare virtuel avec ses boutons et sa grille croisée, à côté d'une carte oscilloscope comparant un rendu candidat à une référence dans une boucle d'évaluation automatisée par des agents Partir d'un langage qui m'intimide Rust est un langage qui m'intimide. Son modèle de propriété, son système de types et ses contraintes sur la mémoire demandent une rigueur que je ne prétends pas encore maîtriser dans tous ses détails. C'est précisément le point de départ de Greybound. Mon hypothèse est que ce qui rend Rust exigeant pour un développeur peut devenir un atout pour l'IA. Le compilateur fournit aux agents une boucle de retour déterministe, précise et difficile à contourner. Une erreur de durée de vie, un partage d'état dangereux ou une incompatibilité de types ne peuvent pas être masqués par une implémentation simplement plausible : le programme doit satisfaire le contrat avant même de pouvoir être évalué. Le véritable but de l'expérience est donc celui-ci : tester jusqu'où des agents peuvent développer de manière autonome un système technique complexe lorsque le langage, les spécifications et les outils d'évaluation leur opposent des garanties fortes. L'audio rend cette expérience particulièrement intéressante. Compiler ne suffit pas à prouver qu'un ampli virtuel sonne ou réagit correctement. Rust ferme une première boucle sur la validité du programme ; un laboratoire Python en ferme une seconde sur la qualité des modèles produits. Modéliser un ampli, pas seulement fabriquer un son Greybound est le support concret de cette expérience : un moteur audio temps réel consacré aux amplis et aux pédales de guitare. Son cœur est écrit en Rust et peut fonctionner comme application autonome ou comme plugin CLAP et VST3. Le modèle Nox30, par exemple, reprend la structure d'un ampli britannique à lampes : étages ECC83, tone stack, déphaseur, quartet d'EL84, alimentation avec sag et transformateur de sortie. L'objectif n'est pourtant ni de reproduire chaque composant dans un simulateur électronique généraliste, ni de capturer l'ampli entier dans une boîte noire. Greybound suit une approche gray-box : la topologie du circuit reste explicite, tandis que les cellules trop coûteuses ou incertaines peuvent être approchées, ajustées ou apprises hors ligne. Le moteur final doit rester déterministe, inspectable et compatible avec les contraintes du temps réel. Greybound devient ainsi le terrain d'une méthode de développement full agentic, pilotée par les spécifications et fermée par une boucle d'évaluation objective. L'autonomie commence par une spécification Une boucle agentique ne devient réellement autonome que si l'agent sait ce qu'il peut modifier, comment vérifier son travail et à quelles conditions il doit le refuser. Dans Greybound, le dépôt lui-même tient ce rôle. Un contrat général définit les invariants d'architecture et de validation. Une base de connaissance versionnée décrit ensuite : - l'architecture du moteur, du routage et du laboratoire ; - la topologie et le niveau de confiance de chaque ampli ou pédale ; - les cellules de circuit réutilisables, leurs unités et leurs hypothèses ; - l'état courant du projet, les décisions prises et les questions ouvertes ; - le protocole, les entrées et les critères de décision de chaque expérience. Les rigs JSON5 ne contiennent que la topologie musicale et les réglages. Les choix d'exécution — fréquence d'échantillonnage, taille de bloc, fichier audio, durée, référence et sortie — restent à l'extérieur. Cette séparation permet à l'agent de générer des variantes temporaires sans modifier silencieusement la configuration de référence. La spécification n'est donc pas un document écrit au début puis oublié. Elle constitue la mémoire opérationnelle du projet. Quand une expérience invalide une hypothèse, le résultat et la limite observée rejoignent cette mémoire avant l'itération suivante. Une boucle de développement fermée À partir d'un objectif borné, l'agent peut exécuter seul toute la campagne : text spécification et hypothèse ↓ modification minimale du DSP Rust ↓ tests, compilation et rendu WAV déterministe ↓ évaluation Python face à SPICE, NAM ou une baseline ↓ rapport, décision et prochaine hypothèse └──────────────────────────────↺ Chaque rendu conserve sa provenance : révision Git, commande exacte, rig source, réglages temporaires, entrée audio, IR, fréquence d'échantillonnage et artefacts produits. Le moniteur relève en parallèle les xruns, le silence, les niveaux RMS et crête, les écrêtages ainsi que des grandeurs internes telles que la tension d'alimentation, les courants ou le flux du transformateur. Si le rendu est instable ou écrête numériquement, l'expérience s'arrête avant toute comparaison sonore. Une sortie invalide ne peut pas devenir un « meilleur modèle » grâce à un score flatteur. Python comme banc de mesure Rust reste responsable du chemin audio temps réel. Python est utilisé hors ligne, là où NumPy, SciPy, ngspice et PyTorch permettent de construire un véritable banc d'essai scientifique. Le laboratoire aligne d'abord le candidat et sa référence en temps et en gain. Il mesure ensuite plusieurs familles de comportements : distance spectrale, résidu après soustraction, enveloppe dynamique, attaques, réponse selon le niveau, harmoniques, intermodulation, bruit, aliasing, phase, décroissance et récupération du sag. Des stimuli synthétiques isolent les phénomènes difficiles à lire dans un simple riff de guitare : sinus par niveaux, deux tons, attaques courtes, hautes fréquences et salves destinées à l'alimentation. Les références changent selon l'échelle observée : - SPICE sert d'oracle électrique pour une cellule ou un nœud précis ; - NAM fournit une référence externe très réaliste pour la chaîne audio complète ; - un rendu Greybound précédent sert de baseline lorsqu'aucune référence plus forte n'existe. Les métriques ne sont jamais réduites à une vérité universelle. Un score composite aide à classer une campagne, mais une amélioration spectrale peut masquer une attaque moins crédible, une dynamique aplatie ou une instabilité. L'agent doit lire les familles de métriques, expliciter les compromis et conserver les conditions de test identiques. Donner les instruments directement aux agents Greybound Lab expose cette chaîne de mesure via MCP. Un agent peut inspecter un rig, lister les entrées et références disponibles, appliquer des réglages temporaires, lancer un rendu borné, analyser sa santé, comparer deux WAV et résumer le rapport produit. Les mêmes expériences sont visibles dans une application de laboratoire destinée aux humains. Les deux interfaces partagent le même artefact versionné : greybound.lab.run.v1. Une expérience lancée par un agent peut donc être rejouée et examinée dans l'interface, tandis qu'un rendu importé par un humain peut entrer dans la boucle agentique. Cette surface est volontairement limitée. Les rendus ont une durée maximale, les variantes ne remplacent pas les rigs sources et les adaptateurs de plugins externes sont déclaratifs. L'autonomie porte sur l'expérimentation, pas sur l'exécution arbitraire de commandes. Un agent doit aussi savoir perdre Le résultat le plus important d'une boucle autonome n'est pas qu'elle produise davantage de code. C'est qu'elle puisse rejeter sa propre solution. Une première expérience a entraîné en Python un petit réseau statique pour reproduire un étage 12AX7 simulé dans SPICE. Il atteignait une racine de l'erreur quadratique moyenne pondérée de 49,555 mV. Ajouter huit échantillons d'historique semblait être une évolution logique ; le résultat a pourtant régressé à 54,698 mV. Le candidat a été écarté automatiquement au lieu d'être conservé parce qu'il était plus sophistiqué. L'itération suivante est revenue à une structure gray-box plus explicite, avec deux états physiques lents et rapides et une non-linéarité bornée. Elle a atteint 35,207 mV, contre 95,318 mV pour la cellule analytique évaluée dans les mêmes conditions. Mais ce succès local n'a pas suffi : une fois replacée dans l'ampli complet, l'amélioration face à NAM restait faible et un autre indicateur se dégradait légèrement. Le modèle est donc resté une sonde d'intégration jusqu'à ce que des validations supplémentaires permettent de figer une variante temps réel. Ce comportement — essayer, mesurer, contredire puis changer de direction — est précisément ce qui transforme une génération de code par IA en processus d'ingénierie agentique. Le rôle humain remonte d'un niveau Dans cette organisation, je n'interviens pas dans chaque micro-itération. Je définis la cible, la crédibilité des références, les contraintes temps réel et les règles de promotion. L'agent prend ensuite en charge la lecture de l'état du projet, l'implémentation, les expériences, l'analyse et la documentation, jusqu'à atteindre un critère d'arrêt ou mettre au jour une incertitude qui exige une décision. L'écoute et le jeu de guitare restent utiles, mais interviennent comme validation produit à des jalons choisis. Ils ne remplacent pas les mesures et ne sont pas nécessaires pour surveiller chaque essai intermédiaire. Greybound m'intéresse ainsi autant pour le son qu'il produit que pour son processus de développement. Le projet montre qu'un agent peut travailler longtemps de manière autonome sur un problème technique complexe, à condition de lui donner une mémoire structurée, des outils bornés et la capacité de prouver qu'une modification est réellement meilleure. Lien - Code source de Greybound"
  },
  {
    "id": "homelab:robine-wake:en",
    "href": "/homelab/robine-wake",
    "locale": "en",
    "kind": "homelab",
    "title": "Robine Wake – Sound and Light Alarm Clock",
    "description": "A local-first Raspberry Pi alarm clock combining gradual sound, Philips Hue lighting, and a physical stop button.",
    "tags": [
      "Elixir",
      "Nerves",
      "React",
      "React Native",
      "ESP32",
      "IoT"
    ],
    "content": "A personal connected alarm clock project: Elixir and Nerves on Raspberry Pi, React and React Native with Expo interfaces, an ESP32 controller, AirPlay audio, and Philips Hue lighting. Personal project Locally controlled alarm · React and React Native interfaces · Physical ESP32 controller Robine Wake: bedroom speakers on a HiFiBerry-equipped Raspberry Pi, talking over Wi-Fi to a small ESP32 stop button An everyday device I wanted an alarm clock that could wake me gradually without depending on a phone or a cloud service. The starting point was a product question: how do you combine a gentle wake-up with a reliable fallback, while keeping the controls simple? A loud alarm can make waking up unpleasant. A gentle alarm can go unnoticed. I wanted a gradual transition, with a stronger signal only if the first one went unanswered. So I built Robine Wake, a local-first alarm clock combining sound, light and a physical stop button — a personal project exploring the intersection of software, audio and hardware. The engineering lesson was straightforward: reliability does not come from one feature. It comes from explicit priorities, observable state and safeguards that remain useful when another component fails. The challenge: a gradual wake-up, an explicit stop Playing a sound at the scheduled time solves only part of the problem. Reaching the end of a volume curve does not confirm that anyone has responded to the alarm. A separate physical button makes that response explicit. The phone remains a configuration interface; it does not need to stay connected for the wake-up sequence to work. I needed two things that seem contradictory: - a gradual transition that did not make the day begin with an attack; - a hard boundary that prevented the gentle wake-up from becoming an optional suggestion. That distinction became the product's core rule: gentle first, uncompromising only when necessary. A gentle path with a real fallback Robine Wake runs on a Raspberry Pi 3 with Nerves, an Elixir platform for embedded systems. A HiFiBerry board owns the bedroom speakers. The clock, alarm schedule and current wake session all live on the device. Losing the Internet does not cancel the morning. The alarm starts before the requested wake time. A chosen melodic track appears at a low volume and rises progressively while Philips Hue lights create a sunrise in the room. At the target time, sound and light have reached their configured level. Then comes the important part: the system does not assume that \"the curve is over\" means \"the alarm has been acknowledged.\" After a plateau, an unanswered alarm switches to a second, deliberately more salient sound. It is not simply the same track turned up forever; it changes meaning. text pre-wake time requested time no response | | | gentle sound ─── progressive rise ─── plateau ─── strong fallback soft light ─────── Hue sunrise ────────┘ The volume has a separate safety ceiling, snooze is limited, and an active session is persisted so that a restart does not conveniently erase it. If an accessory fails, it must degrade without silencing the local alarm. These rules matter more to me than adding another smart feature. The project also experiments with an infrared camera for private, on-device bed-presence detection. It can eventually confirm that the bed has remained empty, but uncertain vision is never allowed to declare victory. When the system does not know, the alarm continues. The ESP32 moves the decision out of reach The strongest safeguard is physical. I built a separate stop control around a Heltec WiFi Kit 32 V1: an ESP32, a small OLED screen and a real button. It displays the time, Wi-Fi quality and whether the alarm is active. When Robine Wake rings, the screen lights up and shows Robine running with a few hearts. Pressing the button sends a versioned dismissal command over Wi-Fi directly to the Raspberry Pi's local network — there is no wired link between the two. The control can be placed away from the bed, so stopping the alarm requires getting up. The interaction stays simple: reach the control, press one button and receive clear confirmation. There is no cloud round-trip in the middle. The ESP32 talks to robine-wake.local, and the screen distinguishes between a stopped alarm, an already quiet room and a network failure. A physical press during an active alarm is handled immediately; it does not ask a barely awake person to perform a long-press ritual correctly. This is the difference between friction and a guardrail. Friction is annoying every day. A guardrail stays almost invisible when the gentle path works and intervenes only when it does not. AirPlay, but never at the expense of the alarm A dedicated box in the bedroom should be useful outside the few minutes when it wakes me. Robine Wake therefore also appears on my local network as Robine Wake Bedroom, a classic AirPlay audio receiver powered by Shairport Sync. I can send music or a podcast from an iPhone or a Mac and use the HiFiBerry setup as a normal bedroom speaker. But this convenience has an explicit priority rule: the alarm always owns the audio output. ALSA access is exclusive. When an alarm starts, the Elixir application suspends the AirPlay receiver before opening the sound device. The current stream may disconnect — and that is intentional. Once the wake session ends, AirPlay starts and advertises itself again. This small detail captures the whole design philosophy. \"Smart\" functionality is welcome only if it cannot weaken the primary promise. A malformed stream, unavailable Wi-Fi or crashed AirPlay process must never delay, attenuate or replace the alarm. Philips Hue for both ends of the night The Hue integration is not limited to sunrise. In the evening, Robine Wake offers a guided 4/6 breathing session: four seconds of inhaling, six seconds of exhaling. The selected Hue room or zone grows brighter during the inhale and fades during the exhale, changing colour with the phase. At the same time, the Raspberry Pi can generate a continuous soundscape of soft noise, light rain and a stable drone whose volume follows the same rhythm. The phone starts or stops the session, but it is not the clock. The cadence belongs to the Raspberry Pi and uses monotonic deadlines, so it continues if the app is locked or disconnected and does not accumulate a little more drift on every breath. Robine Wake also measures local Hue latency to send transitions slightly ahead of their intended boundary. At the end, the previous light state is restored. If an alarm begins during the session, the breathing ritual stops and gives back both Hue and the speakers. Again, the hierarchy is explicit. I use the familiar term \"cardiac coherence\" to describe the 4/6 ritual, but the product makes no therapeutic claim. It is a light and sound pacing guide that helps me slow down before sleep, not a medical device. From firmware to interfaces Elixir was a natural fit because an alarm clock is mostly a collection of concurrent states and failures disguised as a simple object. The scheduler, audio output, Hue bridge, AirPlay receiver and active wake session should be isolated from one another. OTP supervision means a failed accessory does not have to take the clock down with it. Nerves turns that model into an appliance. I control the complete firmware rather than maintaining a general-purpose Linux installation by hand, while retaining the BEAM's supervision model on the Raspberry Pi. The web interface uses React, and the mobile application uses React Native with Expo. Both present explicit device state: the next alarm, the current phase, device health and Hue state. Scheduling and audio control stay on the Raspberry Pi, independently of the interface. The two ecosystems meet through a small, versioned local protocol connecting these interfaces to the Raspberry Pi and the Wi-Fi-connected ESP32 controller. The interface can disappear, reconnect or be replaced without becoming the owner of the alarm clock. Building it with ChatGPT I built Robine Wake with ChatGPT as a programming partner. That sentence can sound as though I typed \"make me an alarm clock\" and waited for a finished product. The actual work was much more interesting. I brought the product goals, the constraints and the final decisions. ChatGPT helped me move quickly between layers: Elixir state machines, Nerves firmware, React and React Native interfaces, ALSA, Hue's local API and ESP32 code. More importantly, the conversation forced implicit assumptions to become explicit scenarios. What happens if the Raspberry Pi restarts while the alarm is sounding? If Hue is unavailable? If AirPlay already owns the speakers? If the stop command is sent twice? If wall-clock time jumps while a snooze timer is running? Those questions became persisted state, idempotent commands, monotonic timers, safety ceilings and tests. AI did not remove engineering responsibility; it increased the surface area I could explore, and made it practical to build across stacks that would otherwise have demanded a much longer detour. The hardware still has the final word. A generated test cannot tell me how loud a speaker is at the pillow, how the OLED looks at night or how a Hue transition feels in the room. Those decisions require calibration, observation and real mornings. An everyday device with clear priorities Robine Wake brings several functions into one device, with a clear order of priorities: the alarm comes first, and every accessory must be able to fail without preventing it from ringing. Its success criterion is concrete: start gently, then use a stronger signal only when the first one goes unanswered. Be useful as an AirPlay speaker during the day. Offer guided light and sound in the evening. Keep every essential decision local. And never confuse an elegant animation with confirmation that the wake-up sequence is complete. Building an alarm clock turned into an exercise in system design: coordinating independent components, handling failures and testing the complete experience on real hardware. Hardware validation is ongoing — sound levels, screen readability at night, and button behaviour in the room. The source code and hardware files live in the Robine Wake repository. It is still private while I finish validating the hardware, but I intend to document and open the reusable parts progressively."
  },
  {
    "id": "homelab:robine-wake:fr",
    "href": "/homelab/robine-wake",
    "locale": "fr",
    "kind": "homelab",
    "title": "Robine Wake – Réveil sonore et lumineux",
    "description": "Un réveil local sur Raspberry Pi qui associe son progressif, lumière Philips Hue et bouton d'arrêt physique.",
    "tags": [
      "Elixir",
      "Nerves",
      "React",
      "React Native",
      "ESP32",
      "IoT"
    ],
    "content": "Projet personnel de réveil connecté : Elixir et Nerves sur Raspberry Pi, interfaces React et React Native avec Expo, commande ESP32, audio AirPlay et éclairage Philips Hue. Projet personnel Alarme pilotée en local · Interfaces React et React Native · Commande physique ESP32 Robine Wake : enceintes de chambre sur un Raspberry Pi équipé d'une carte HiFiBerry, relié en Wi-Fi à un petit bouton d'arrêt ESP32 Un objet pour le quotidien Je voulais un réveil capable de me réveiller progressivement, sans dépendre d'un téléphone ou d'un service cloud. Le point de départ était une question de conception : comment associer un réveil doux à un garde-fou fiable, tout en conservant des commandes simples ? Un réveil fort peut rendre le réveil désagréable. Un réveil doux peut passer inaperçu. Je cherchais une transition progressive, avec un signal plus marqué uniquement si le premier restait sans réponse. J'ai donc construit Robine Wake, un réveil local qui associe son, lumière et bouton d'arrêt physique — un projet personnel construit pour explorer la rencontre entre logiciel, audio et matériel. La leçon d'ingénierie était simple : la fiabilité ne vient pas d'une seule fonctionnalité. Elle repose sur des priorités explicites, un état observable et des garde-fous qui restent utiles lorsqu'un autre composant échoue. Le défi : un réveil progressif, un arrêt explicite Jouer un son à l'heure prévue ne résout qu'une partie du problème. Atteindre la fin d'une courbe de volume ne confirme pas qu'une personne a répondu à l'alarme. Un bouton physique séparé rend cette réponse explicite. Le téléphone reste une interface de configuration : il n'a pas besoin de rester connecté pour que la séquence de réveil fonctionne. Il me fallait deux choses qui semblent contradictoires : - une transition progressive qui ne transforme pas le début de journée en agression ; - une limite ferme qui empêche le réveil doux de devenir une simple suggestion. Cette distinction est devenue la règle centrale du produit : doux d'abord, non négociable seulement si nécessaire. Un chemin doux, avec un vrai filet de sécurité Robine Wake fonctionne sur un Raspberry Pi 3 avec Nerves, la plateforme Elixir pour les systèmes embarqués. Une carte HiFiBerry pilote les enceintes de la chambre. L'horloge, la programmation des alarmes et la session de réveil en cours vivent sur l'appareil. Perdre Internet n'annule pas le matin. Le réveil commence avant l'heure demandée. Une piste mélodique choisie apparaît à faible volume puis monte progressivement, tandis que les lampes Philips Hue créent un lever de lumière dans la pièce. À l'heure cible, le son et la lumière ont atteint leur niveau configuré. Vient alors la partie importante : le système ne suppose pas que « la courbe est terminée » signifie « l'alarme a été acquittée ». Après un plateau, une alarme restée sans réponse passe à un second son, volontairement plus saillant. Ce n'est pas seulement la même musique de plus en plus forte : le signal change de sens. text avant le réveil heure demandée aucune réponse | | | son doux ─────── montée progressive ─── plateau ─── garde-fou fort lumière ───────── lever de lumière Hue ─────┘ Le volume possède un plafond de sécurité indépendant, le nombre de répétitions est limité, et la session active est persistée pour qu'un redémarrage ne l'efface pas opportunément. Si un accessoire tombe en panne, il doit se dégrader sans faire taire l'alarme locale. Ces règles comptent davantage pour moi que l'ajout d'une énième fonction « intelligente ». Le projet expérimente aussi avec une caméra infrarouge pour détecter localement et sans cloud la présence dans le lit. Elle pourra confirmer que le lit est resté vide, mais une vision incertaine n'a jamais le droit de crier victoire. Quand le système ne sait pas, l'alarme continue. L'ESP32 déplace la décision hors de portée Le garde-fou le plus fort est physique. J'ai fabriqué une commande d'arrêt séparée autour d'un Heltec WiFi Kit 32 V1 : un ESP32, un petit écran OLED et un vrai bouton. Il affiche l'heure, la qualité du Wi-Fi et l'état du réveil. Quand Robine Wake sonne, l'écran s'allume et montre Robine en train de courir, entourée de quelques cœurs. Un appui envoie, en Wi-Fi et directement sur le réseau local, une commande d'arrêt versionnée au Raspberry Pi — aucun câble ne relie les deux appareils. La commande peut être placée à distance du lit, afin que l'arrêt du réveil demande de se lever. L'interaction reste simple : rejoindre la commande, appuyer une fois et obtenir une confirmation claire. Aucun aller-retour par le cloud ne s'interpose. L'ESP32 parle à robine-wake.local, et l'écran distingue un réveil arrêté, une pièce déjà silencieuse et une panne réseau. Pendant une alarme active, l'appui est traité immédiatement : inutile d'exiger d'une personne à peine réveillée qu'elle réussisse un appui long au bon moment. C'est la différence entre une friction et un garde-fou. La friction agace tous les jours. Le garde-fou reste presque invisible quand le réveil doux suffit, et intervient uniquement lorsqu'il échoue. AirPlay, mais jamais au détriment du réveil Une boîte dédiée dans la chambre devrait servir au-delà des quelques minutes où elle me réveille. Robine Wake apparaît donc aussi sur mon réseau local sous le nom Robine Wake Bedroom, comme récepteur audio AirPlay classique propulsé par Shairport Sync. Je peux envoyer de la musique ou un podcast depuis un iPhone ou un Mac et utiliser le système HiFiBerry comme une enceinte de chambre ordinaire. Mais cette commodité obéit à une règle de priorité explicite : le réveil possède toujours la sortie audio. L'accès à ALSA est exclusif. Quand une alarme commence, l'application Elixir suspend le récepteur AirPlay avant d'ouvrir la carte son. Le flux en cours peut être déconnecté — c'est volontaire. Dès que la session de réveil se termine, AirPlay redémarre et s'annonce de nouveau sur le réseau. Ce détail résume la philosophie du projet. Une fonction « intelligente » n'est bienvenue que si elle ne fragilise pas la promesse principale. Un flux mal formé, un Wi-Fi absent ou un processus AirPlay en panne ne doit jamais retarder, atténuer ou remplacer l'alarme. Philips Hue aux deux extrémités de la nuit L'intégration Hue ne sert pas uniquement au lever de lumière. Le soir, Robine Wake propose une séance de respiration guidée 4/6 : quatre secondes d'inspiration, six secondes d'expiration. La pièce ou la zone Hue choisie gagne en luminosité pendant l'inspiration, puis redescend pendant l'expiration, en changeant de couleur avec la phase. En parallèle, le Raspberry Pi peut générer une ambiance continue faite d'un souffle doux, d'une pluie légère et d'un bourdon stable dont le volume suit le même rythme. Le téléphone lance ou arrête la séance, mais il n'en est pas l'horloge. La cadence appartient au Raspberry Pi et repose sur des échéances monotones : elle continue si l'application est verrouillée ou déconnectée, sans accumuler un peu plus de dérive à chaque respiration. Robine Wake mesure même la latence locale de Hue pour envoyer les transitions légèrement avant la frontière visée. À la fin, l'état précédent des lampes est restauré. Si une alarme commence pendant la séance, le rituel de respiration s'arrête et libère Hue comme les enceintes. Là encore, la hiérarchie est explicite. J'emploie le terme courant de « cohérence cardiaque » pour parler de ce rituel 4/6, mais le produit ne fait aucune promesse thérapeutique. C'est un guide lumineux et sonore qui m'aide à ralentir avant de dormir, pas un dispositif médical. Du firmware aux interfaces Elixir s'est imposé naturellement parce qu'un réveil est surtout un ensemble d'états concurrents et de pannes, déguisé en objet simple. Le planificateur, la sortie audio, le pont Hue, le récepteur AirPlay et la session de réveil active doivent pouvoir vivre et échouer indépendamment. Grâce à la supervision OTP, la panne d'un accessoire n'a pas à emporter l'horloge avec elle. Nerves transforme ce modèle en appareil. Je maîtrise le firmware complet au lieu d'entretenir à la main une distribution Linux généraliste, tout en conservant sur le Raspberry Pi le modèle de supervision de la BEAM. L'interface web utilise React, et l'application mobile React Native avec Expo. Elles présentent l'état explicite de l'appareil : la prochaine alarme, la phase en cours, la santé de l'appareil et l'état de Hue. La programmation et le pilotage audio restent sur le Raspberry Pi, indépendamment de l'interface. Les deux univers se rejoignent autour d'un petit protocole local versionné, qui relie ces interfaces au Raspberry Pi et à la commande ESP32 connectée en Wi-Fi. L'interface peut disparaître, se reconnecter ou être remplacée sans devenir propriétaire du réveil. Le construire avec ChatGPT J'ai construit Robine Wake avec ChatGPT comme partenaire de programmation. Cette phrase peut donner l'impression que j'ai écrit « fais-moi un réveil » avant d'attendre un produit terminé. Le travail réel était bien plus intéressant. J'ai apporté les objectifs du produit, les contraintes et les décisions finales. ChatGPT m'a aidé à passer rapidement entre les couches : machines à états Elixir, firmware Nerves, interfaces React et React Native, ALSA, API locale de Hue et code ESP32. Surtout, la conversation a transformé des suppositions implicites en scénarios explicites. Que se passe-t-il si le Raspberry Pi redémarre pendant que le réveil sonne ? Si Hue est indisponible ? Si AirPlay possède déjà les enceintes ? Si la commande d'arrêt arrive deux fois ? Si l'heure civile saute pendant un minuteur de répétition ? Ces questions sont devenues de l'état persisté, des commandes idempotentes, des horloges monotones, des plafonds de sécurité et des tests. L'IA n'a pas supprimé la responsabilité d'ingénierie ; elle a élargi le terrain que je pouvais explorer et rendu réaliste la construction d'un système à travers plusieurs stacks qui m'auraient autrement imposé un détour bien plus long. Le matériel conserve le dernier mot. Un test généré ne peut pas me dire quel niveau sonore arrive réellement jusqu'à l'oreiller, à quoi ressemble l'OLED dans le noir ou comment une transition Hue est ressentie dans la pièce. Ces décisions demandent de la calibration, de l'observation et de vrais matins. Un appareil du quotidien, avec des priorités claires Robine Wake rassemble plusieurs fonctions dans un appareil, avec un ordre de priorité clair : le réveil passe avant tout, et chaque accessoire doit pouvoir tomber en panne sans l'empêcher de sonner. Son critère de réussite est concret : commencer en douceur, puis utiliser un signal plus marqué uniquement si le premier reste sans réponse. Servir d'enceinte AirPlay pendant la journée. Proposer un accompagnement lumineux et sonore le soir. Garder chaque décision essentielle en local. Et ne jamais confondre une animation élégante avec la confirmation que la séquence de réveil est terminée. Construire un réveil est devenu un exercice de conception de système : coordonner des composants indépendants, gérer les pannes et tester l'expérience complète sur du matériel réel. La validation matérielle se poursuit : niveau sonore, lisibilité de l'écran la nuit et comportement du bouton dans la pièce. Le code source et les fichiers matériels vivent dans le dépôt Robine Wake. Il est encore privé le temps de terminer la validation du matériel, mais je compte documenter et ouvrir progressivement les parties réutilisables."
  },
  {
    "id": "homelab:soma26:en",
    "href": "/homelab/soma26",
    "locale": "en",
    "kind": "homelab",
    "title": "Soma26 – Generative Neuro-Acoustic Engine",
    "description": "Generative neuro-acoustic engine for focus, relaxation, and sleep.",
    "tags": [
      "Product",
      "Audio",
      "Rust",
      "DSP",
      "Neuroscience"
    ],
    "content": "Soma26 is a real-time audio application that uses brainwave entrainment to guide the listener into desired mental states using psychoacoustic principles backed by neuroscience research. Soma26 Base59 Real-time generative audio · Cross-platform Rust core · Neuroscience-backed entrainment Soma26: headphones emitting sound waves next to a brain with a modulated brainwave line, illustrating neuro-acoustic entrainment What is Soma26 Soma26 is a real-time audio application that uses brainwave entrainment to guide the listener into desired mental states — deep focus, creative flow, relaxation, or restorative sleep. Rather than playing pre-recorded tracks, it generates an infinite, evolving soundscape on the fly using psychoacoustic principles backed by neuroscience research. The idea Our brains naturally synchronize with rhythmic external stimuli — a phenomenon called neural entrainment. Soma26 exploits this by embedding precisely calibrated amplitude modulations into a layered audio texture. Unlike simple binaural beat generators, it applies modulation selectively to the 200 Hz–1 kHz frequency band (where entrainment is most effective and least fatiguing), while leaving bass and treble untouched. The result is a sound that feels like music, not a clinical tone. Each mode targets a specific brainwave range: - Focus — Beta waves (16 Hz) for sustained concentration - Creative — Theta waves (6 Hz) for intuition and flow - Relax — Alpha waves (10 Hz) for calm awareness - Sleep — Delta waves (2.5 Hz) for deep rest How it works The audio pipeline stacks five functional layers: a noise blanket (pink/brown noise) that masks the environment, harmonic pads built from slow-evolving chord progressions, a spectral gate that applies the entrainment modulation to the mid-band only, a spatial processor that externalizes the sound (so it feels like it's coming from the room, not inside your head), and a subtle binaural carrier underneath. A probabilistic arranger — inspired by Brain.fm's approach — selects and transitions between musical motifs, varying density and intensity over time so the soundscape never loops or grows stale. Chord progressions follow a harmonic grammar based on Martin Rohrmeier's linguistic model of tonal harmony, generating musically coherent sequences while staying within a low-cognitive-load pentatonic framework. Technology The core engine is written in Rust using the fundsp functional DSP library for real-time audio synthesis. It runs as a CLI tool, a WebAssembly module powering the web app, a native iOS framework (via UniFFI), and an Android/KMP library — all sharing the same Rust core. The web frontend is built with Next.js and the mobile app uses Kotlin Multiplatform with a native Swift layer on iOS. Links - Web app — Try it in your browser - Landing page — Learn the science"
  },
  {
    "id": "homelab:soma26:fr",
    "href": "/homelab/soma26",
    "locale": "fr",
    "kind": "homelab",
    "title": "Soma26 – Moteur neuro-acoustique génératif",
    "description": "Moteur neuro-acoustique génératif pour la concentration, la relaxation et le sommeil.",
    "tags": [
      "Produit",
      "Audio",
      "Rust",
      "DSP",
      "Neurosciences"
    ],
    "content": "Soma26 est une application audio temps réel qui utilise l'entraînement des ondes cérébrales pour guider l'auditeur vers des états mentaux ciblés, grâce à des principes psychoacoustiques étayés par la recherche en neurosciences. Soma26 Base59 Audio génératif temps réel · Cœur Rust cross-plateforme · Entraînement cérébral scientifique Soma26 : un casque émettant des ondes sonores à côté d'un cerveau traversé par une onde cérébrale modulée, illustrant l'entraînement neuro-acoustique Qu'est-ce que Soma26 Soma26 est une application audio temps réel qui utilise l'entraînement des ondes cérébrales pour guider l'auditeur vers des états mentaux ciblés — concentration profonde, flux créatif, relaxation ou sommeil réparateur. Plutôt que de diffuser des pistes pré-enregistrées, elle génère un paysage sonore infini et évolutif à la volée, en s'appuyant sur des principes psychoacoustiques étayés par la recherche en neurosciences. L'idée Nos cerveaux se synchronisent naturellement avec des stimuli rythmiques externes — un phénomène appelé entraînement neuronal. Soma26 exploite ce mécanisme en intégrant des modulations d'amplitude précisément calibrées dans une texture audio multicouche. Contrairement aux simples générateurs de battements binauraux, la modulation est appliquée sélectivement à la bande 200 Hz–1 kHz (là où l'entraînement est le plus efficace et le moins fatigant), tout en laissant les graves et les aigus intacts. Le résultat : un son qui ressemble à de la musique, pas à une tonalité clinique. Chaque mode cible une bande d'ondes cérébrales spécifique : - Focus — Ondes bêta (16 Hz) pour la concentration soutenue - Créatif — Ondes thêta (6 Hz) pour l'intuition et le flux - Relax — Ondes alpha (10 Hz) pour la sérénité consciente - Sommeil — Ondes delta (2,5 Hz) pour le repos profond Comment ça fonctionne Le pipeline audio empile cinq couches fonctionnelles : une couverture de bruit (bruit rose/brun) qui masque l'environnement, des nappes harmoniques construites à partir de progressions d'accords lentement évolutives, un filtre spectral qui applique la modulation d'entraînement à la bande médiane uniquement, un processeur spatial qui externalise le son (pour qu'il semble venir de la pièce, pas de l'intérieur de la tête), et une porteuse binaurale subtile en dessous. Un arrangeur probabiliste — inspiré de l'approche de Brain.fm — sélectionne et enchaîne les motifs musicaux, en variant la densité et l'intensité au fil du temps pour que le paysage sonore ne boucle jamais et ne devienne pas monotone. Les progressions d'accords suivent une grammaire harmonique basée sur le modèle linguistique de l'harmonie tonale de Martin Rohrmeier, générant des séquences musicalement cohérentes tout en restant dans un cadre pentatonique à faible charge cognitive. Technologie Le cœur du moteur est écrit en Rust avec la bibliothèque DSP fonctionnelle fundsp pour la synthèse audio temps réel. Il s'exécute sous forme d'outil CLI, de module WebAssembly pour l'application web, de framework natif iOS (via UniFFI) et de bibliothèque Android/KMP — tous partageant le même cœur Rust. Le frontend web est construit avec Next.js et l'application mobile utilise Kotlin Multiplatform avec une couche Swift native sur iOS. Liens - Application web — Essayez dans votre navigateur - Page de présentation — Découvrir la science"
  },
  {
    "id": "thought:kent-beck:en",
    "href": "/thoughts/kent-beck",
    "locale": "en",
    "kind": "thought",
    "title": "Pragmatism or Perfectionism? What Kent Beck and Uncle Bob Teach Us About Software Engineering",
    "description": "Kent Beck and Robert C. Martin both signed the Agile Manifesto, but their approaches to technical excellence diverge in fascinating ways. The 3X model, code optionality, and software craftsmanship: what combining their philosophies teaches us about modern engineering.",
    "tags": [
      "kent-beck",
      "tdd",
      "agile",
      "extreme-programming",
      "software-craftsmanship"
    ],
    "content": "In software development, finding the right balance between technical quality and delivering business value is a daily challenge. Two figures have deeply shaped how I think about it: Kent Beck, creator of Extreme Programming (XP) and the rediscoverer of Test-Driven Development, and Robert C. Martin (aka Uncle Bob), the leading voice behind Clean Architecture and the Software Craftsmanship movement. Both signed the original Agile Manifesto. But their approaches to technical excellence differ in ways that are worth exploring — and combining them gives a more complete picture than either one alone. Business context first: the 3X model vs. the cleanliness dogma Uncle Bob is the strongest advocate for software craftsmanship. His approach — popularized through concepts like Clean Architecture — argues that code must be kept as clean as possible at all times. Rigorous practices like TDD are not optional. They are what separates professional engineers from those producing mediocrity. Kent Beck approaches software design from a very different angle, one driven by the product lifecycle. He developed the 3X model — Explore, Expand, Extract — after his time at Facebook. The core idea is simple: the rules of development change depending on where the product is. - Explore. The goal is validated learning. Wild ideas and hacking are not just acceptable — they are recommended. Most experiments will be thrown away. Enforcing a heavy Clean Architecture at this stage is an economic mistake. - Expand / Extract. Once the product is growing and needs to be sustainable, efficiency, removing bottlenecks, and technical rigor become critical. The lesson: use Clean Architecture's discipline for Extract-phase systems where longevity matters, but apply Beck's Explore pragmatism when you need to test an idea in the market quickly. Code as an economic investment In his book Tidy First?, Kent Beck connects software design directly to financial concepts — time value of money, optionality. A system that is easy to change has value because it gives you options to respond to uncertainty. It is a lens few engineers use, and it changes how you prioritize technical work. But unlike some craftsmanship advocates who chase endless perfection, Beck warns against over-tidying. He compares code cleanup to Pringles: once you start, it is hard to stop. Discipline also means knowing when to put the scissors down. His most useful advice stays with you: make the change easy, then make the easy change. Refactor to create viable economic options — not out of technical vanity. A shared foundation: technical excellence is not optional Despite their different perspectives — Beck's economic pragmatism vs. Uncle Bob's craftsman's rigor — both agree on one crucial point: technical excellence is not optional. It is the foundation of real agility. Uncle Bob has often lamented seeing the Agile movement drift away from engineering practices toward pure project management. Beck shares that diagnosis: sustainable speed comes from practices like TDD and continuous refactoring — not from shorter and shorter sprints on top of accumulated technical debt. This shared ground may be their most important contribution. They both remind us that speed and quality are not in opposition. Technical quality is speed, over time. What combining them teaches Being a good developer today means navigating between these two philosophies. Mastering Clean Architecture to build robust, decoupled systems — while also applying Beck's sharp sense of economics and context to avoid over-engineering things that don't need it. This balance between high-quality engineering and product-oriented pragmatism is what separates teams that last from teams that burn out repaying debt or gold-plating prototypes. Beck and Uncle Bob, together, sketch out what genuinely mature software engineering could look like."
  },
  {
    "id": "thought:kent-beck:fr",
    "href": "/thoughts/kent-beck",
    "locale": "fr",
    "kind": "thought",
    "title": "Pragmatisme ou Perfectionnisme ? Ce que Kent Beck et Uncle Bob nous apprennent sur l'ingénierie logicielle",
    "description": "Kent Beck et Robert C. Martin ont tous deux signé le Manifeste Agile, mais leurs approches de l'excellence technique divergent de façon fascinante. Modèle 3X, optionnalité du code, et artisanat logiciel : ce que la combinaison de leurs philosophies enseigne sur l'ingénierie moderne.",
    "tags": [
      "kent-beck",
      "tdd",
      "agile",
      "extreme-programming",
      "software-craftsmanship"
    ],
    "content": "Dans le monde du développement logiciel, trouver le juste équilibre entre la qualité technique et la livraison de valeur métier est un défi quotidien. Deux figures ont profondément influencé ma façon d'y répondre : Kent Beck, créateur de l'Extreme Programming (XP) et redécouvreur du Test-Driven Development, et Robert C. Martin (alias Uncle Bob), figure de proue de la Clean Architecture et du mouvement Software Craftsmanship. Bien que tous deux signataires du Manifeste Agile originel, leurs approches pour atteindre l'excellence technique présentent des nuances fascinantes — et leur combinaison dessine une philosophie plus complète que chacune prise séparément. Le contexte métier avant tout : le modèle 3X face au dogme de la propreté Uncle Bob est le grand défenseur de l'artisanat logiciel. Son approche, popularisée par des concepts comme la Clean Architecture, insiste sur la nécessité de maintenir un code aussi propre que possible en tout temps, en appliquant des pratiques techniques rigoureuses comme le TDD pour éviter la \"médiocrité\". De son côté, Kent Beck aborde la conception logicielle sous un prisme très différent, souvent dicté par le cycle de vie du produit. Il a théorisé le modèle 3X — Explore, Expand, Extract — suite à son passage chez Facebook. Ce modèle stipule que les règles du développement changent selon la phase du produit : - Explore. Le but est l'apprentissage validé. Dans cette phase, les idées folles et le \"hacking\" sont non seulement acceptables, mais recommandés — la plupart des expérimentations seront abandonnées. Imposer une Clean Architecture lourde ici serait une erreur économique. - Expand / Extract. Lorsque le produit grandit et doit être rentable, c'est là que l'efficacité, la suppression des goulots d'étranglement et la rigueur technique deviennent primordiales. L'enseignement : utiliser la rigueur de la Clean Architecture pour les systèmes en phase Extract où la pérennité est vitale, mais savoir faire preuve du pragmatisme Explore de Beck lorsqu'il s'agit de tester rapidement une idée sur le marché. Le code comme investissement économique Dans son ouvrage Tidy First?, Kent Beck relie directement la conception logicielle à des concepts financiers — la valeur temporelle de l'argent, l'optionnalité. Un système facile à modifier a de la valeur parce qu'il offre des options pour répondre à l'incertitude. C'est une grille de lecture que peu d'ingénieurs utilisent, et qui change profondément la façon de prioriser le travail technique. Mais contrairement à la quête parfois infinie de perfection de certains artisans logiciels, Beck met en garde contre le nettoyage excessif. Il compare d'ailleurs le tidying à des chips Pringles : une fois que vous commencez, il est difficile de s'arrêter. La discipline, c'est aussi savoir quand poser les ciseaux. Son conseil le plus utile reste celui-ci : rendre d'abord le changement facile, puis faire le changement facile. Refactoriser pour créer des options économiques viables, et non par vanité technique. Un terrain d'entente fondamental : l'excellence technique Malgré leurs différences de perspective — le pragmatisme économique de Beck face à la rigueur de l'artisan d'Uncle Bob — les deux experts se rejoignent sur un point crucial : l'excellence technique n'est pas optionnelle. C'est la fondation de la véritable agilité. Uncle Bob s'est souvent désolé de voir le mouvement Agile s'éloigner des pratiques d'ingénierie au profit de la simple gestion de projet. Beck partage ce diagnostic : la vitesse durable s'obtient grâce à des pratiques comme le TDD et la refactorisation continue — pas à des sprints de plus en plus courts avec de la dette technique accumulée. Ce terrain commun est peut-être leur contribution la plus importante : rappeler que la vitesse et la qualité ne sont pas en opposition. La qualité, au sens technique du terme, est la vitesse sur le long terme. Ce que leur combinaison enseigne Être un bon développeur aujourd'hui, c'est savoir naviguer entre ces deux philosophies. Maîtriser les principes de la Clean Architecture pour concevoir des systèmes robustes et découplés — et simultanément appliquer le sens aiguisé de l'économie et du contexte de Beck pour ne pas sur-ingénierer ce qui n'en a pas besoin. C'est cet équilibre entre ingénierie de haute qualité et pragmatisme orienté produit qui distingue les équipes qui durent de celles qui s'épuisent à rembourser de la dette ou à sur-architecturer des prototypes. Beck et Uncle Bob, ensemble, dessinent les contours de ce que pourrait être une ingénierie logicielle vraiment mature."
  },
  {
    "id": "thought:rich-hickey:en",
    "href": "/thoughts/rich-hickey",
    "locale": "en",
    "kind": "thought",
    "title": "Rich Hickey — The quiet radical who rethought programming",
    "description": "The creator of Clojure and Datomic didn't just build tools — he challenged the assumptions baked into mainstream software development. Simplicity, immutability, and data-first thinking: the ideas that made him one of the most influential minds in our field.",
    "tags": [
      "clojure",
      "simplicity",
      "functional-programming"
    ],
    "content": "Rich Hickey doesn't do keynotes at every conference. He doesn't have a YouTube course empire or a bestselling book series. He gave a handful of talks, built two transformative pieces of software, and changed how thousands of developers think about their craft. That's a different kind of influence — quieter, deeper, and harder to shake off once it lands. Before Clojure Hickey spent over twenty years as a consultant and developer, working primarily in C++ and Java. He built scheduling systems, broadcast automation software, and audio analysis tools — real-world, production systems with hard constraints. During that time, he grew increasingly frustrated with the accidental complexity baked into mainstream object-oriented programming. Mutable state everywhere. Complex inheritance hierarchies that modeled nothing real. Concurrency treated as an afterthought, patched over with locks and synchronized blocks. He didn't write a blog post about it. He spent two years designing a language. Clojure: a Lisp for the real world Released in 2007, Clojure was Rich Hickey's answer to a question most people weren't asking: what if we took immutability seriously? Clojure runs on the JVM, giving it access to the entire Java ecosystem — a pragmatic choice that set it apart from academic functional languages. But the design decisions are what matter: - Immutable data structures by default. Values don't change. If you want a new version, you create one. This eliminates entire categories of bugs related to shared mutable state. - Persistent data structures. Clojure's vectors, maps, and sets share structure under the hood, making immutability efficient — not a performance tax. - Code as data. As a Lisp, Clojure treats code as data structures (homoiconicity), enabling macros that transform programs as naturally as functions transform values. - Concurrency primitives built in. Atoms, refs, agents, and the STM (Software Transactional Memory) provide coordinated state management without manual locking. - A strong opinion on state. Hickey drew a hard line between identity (a stable reference over time) and state (the value an identity holds at a point in time). This distinction, laid out in his talk Are We There Yet?, is one of the most important conceptual contributions to modern programming. Clojure didn't try to be everything. It didn't have classes, didn't have type annotations (initially), didn't try to look like Java. It traded familiarity for coherence — and attracted developers who were tired of fighting their tools. The talks that changed minds Hickey's real superpower is his ability to take a concept everyone thinks they understand and show them they don't. His talks are dense, precise, and rewatchable: - Simple Made Easy (2011) — The distinction between simple (not interleaved) and easy (familiar). The most important 36 minutes in software engineering education. - Are We There Yet? (2009) — A deep examination of time, identity, and state. Why mutable objects are a broken model of reality. - The Value of Values (2012) — Why values (immutable facts) are superior to places (mutable references) as the foundation of information systems. - Hammock Driven Development (2010) — A case for thinking before coding. Step away from the keyboard. Let the subconscious work. The best debugger is a hammock. - The Language of the System (2012) — Why the properties of a good system (queues, immutability, loose coupling) should be reflected in the programming language itself. - Spec-ulation (2016) — A sharp critique of semantic versioning and breaking changes. If you break your consumers, you didn't version — you created a new thing. Each talk starts from first principles. Hickey doesn't say \"here's a pattern.\" He says \"here's how we think about this problem — and here's why that thinking is wrong.\" Then he proposes an alternative that, once heard, is hard to unsee. Datomic: rethinking the database In 2012, Hickey and his team released Datomic, a database built on the same principles as Clojure: - Immutable facts. The database stores facts over time. Nothing is ever updated or deleted — new facts are asserted, old facts remain. The entire history of your data is always available. - Separation of reads and writes. Queries don't compete with transactions. Reads happen against an immutable snapshot. - Datalog query language. Instead of SQL, Datomic uses a logic-based query language that composes naturally and avoids the impedance mismatch between code and queries. Datomic never became mainstream, but its ideas seeped into the industry. Event sourcing, append-only logs, temporal databases — the conversation shifted partly because Hickey built a working system that proved these ideas weren't just academic. The philosophy underneath What sets Hickey apart from most language designers is that he starts from philosophy, not syntax. His influences — Alfred North Whitehead's process philosophy, the information theory distinction between information and mechanism — aren't typical programming references. His core thesis is deceptively simple: most software complexity is accidental, not essential. We complicate things not because the problem demands it, but because our tools and habits push us toward interleaving concerns. Mutable objects encourage coupling. Inheritance encourages taxonomy where there is none. ORMs encourage pretending databases are object stores. The antidote, in Hickey's view, is always the same: go back to data. Data is simple. Data is transparent. Data doesn't hide behind methods. Data composes. If your architecture is built on plain data — maps, vectors, sets — it's easier to inspect, test, serialize, and reason about than any class hierarchy. There's also a deeply artisanal dimension to Hickey's work that you rarely find in other language creators. For him, coding isn't assembling bricks — it's designing living systems. A program isn't a frozen artifact you ship and forget: it's an organism that evolves, that accumulates facts over time, that must remain comprehensible as it grows. That's why immutability isn't a technical choice for Hickey — it's a design ethic. Every decision in Clojure and Datomic reflects this obsession with building systems that age well: that stay malleable without becoming brittle, that welcome change without losing their integrity. Like a cabinetmaker who studies the grain before making a cut, Hickey spends more time thinking about the problem than writing code — and it shows in every seam of his creations. Why he matters Rich Hickey matters because he proved that simplicity is not a compromise. You don't have to give up power to gain clarity. You don't have to give up performance to gain immutability. You don't have to give up practicality to gain elegance. He also proved something less technical but equally important: you don't have to be loud to be influential. No Twitter storms, no flame wars, no hot takes. Just a language, a database, and a handful of talks — each one saying: think harder, complect less, and trust the data. That's enough. It's more than enough."
  },
  {
    "id": "thought:rich-hickey:fr",
    "href": "/thoughts/rich-hickey",
    "locale": "fr",
    "kind": "thought",
    "title": "Rich Hickey — Le radical discret qui a repensé la programmation",
    "description": "Le créateur de Clojure et Datomic n'a pas juste construit des outils — il a remis en question les hypothèses ancrées dans le développement logiciel mainstream. Simplicité, immutabilité et pensée data-first : les idées qui en font l'un des esprits les plus influents de notre domaine.",
    "tags": [
      "clojure",
      "simplicity",
      "functional-programming"
    ],
    "content": "Rich Hickey ne fait pas de keynotes à toutes les conférences. Il n'a pas d'empire YouTube ni de série de best-sellers. Il a donné une poignée de talks, construit deux logiciels transformateurs, et changé la façon de penser de milliers de développeurs. C'est un autre type d'influence — plus silencieuse, plus profonde, et plus difficile à oublier une fois qu'elle a pris racine. Avant Clojure Hickey a passé plus de vingt ans comme consultant et développeur, travaillant principalement en C++ et Java. Il a construit des systèmes de planification, des logiciels d'automatisation de diffusion et des outils d'analyse audio — des systèmes de production réels avec des contraintes dures. Pendant cette période, il est devenu de plus en plus frustré par la complexité accidentelle ancrée dans la programmation orientée objet mainstream. De l'état mutable partout. Des hiérarchies d'héritage complexes qui ne modélisaient rien de réel. La concurrence traitée comme une réflexion après coup, colmatée avec des locks et des blocs synchronized. Il n'a pas écrit un article de blog à ce sujet. Il a passé deux ans à concevoir un langage. Clojure : un Lisp pour le monde réel Sorti en 2007, Clojure était la réponse de Rich Hickey à une question que la plupart des gens ne posaient pas : et si on prenait l'immutabilité au sérieux ? Clojure tourne sur la JVM, ce qui lui donne accès à tout l'écosystème Java — un choix pragmatique qui le distingue des langages fonctionnels académiques. Mais ce sont les décisions de conception qui comptent : - Structures de données immutables par défaut. Les valeurs ne changent pas. Si vous voulez une nouvelle version, vous en créez une. Ça élimine des catégories entières de bugs liés à l'état mutable partagé. - Structures de données persistantes. Les vecteurs, maps et sets de Clojure partagent leur structure sous le capot, rendant l'immutabilité efficace — pas une taxe sur la performance. - Le code comme donnée. En tant que Lisp, Clojure traite le code comme des structures de données (homoiconicité), permettant des macros qui transforment les programmes aussi naturellement que les fonctions transforment les valeurs. - Primitives de concurrence intégrées. Atoms, refs, agents et le STM (Software Transactional Memory) fournissent une gestion coordonnée de l'état sans verrouillage manuel. - Une opinion forte sur l'état. Hickey a tracé une ligne nette entre identité (une référence stable dans le temps) et état (la valeur qu'une identité détient à un instant donné). Cette distinction, exposée dans son talk Are We There Yet?, est l'une des contributions conceptuelles les plus importantes à la programmation moderne. Clojure n'a pas essayé d'être tout. Pas de classes, pas d'annotations de types (initialement), pas d'effort pour ressembler à Java. Il a échangé la familiarité contre la cohérence — et a attiré des développeurs fatigués de se battre contre leurs outils. Les talks qui ont changé les esprits Le vrai super-pouvoir de Hickey est sa capacité à prendre un concept que tout le monde pense comprendre et à montrer que ce n'est pas le cas. Ses talks sont denses, précis et supportent les revisionnages : - Simple Made Easy (2011) — La distinction entre simple (non entrelacé) et facile (familier). Les 36 minutes les plus importantes en enseignement du génie logiciel. - Are We There Yet? (2009) — Un examen approfondi du temps, de l'identité et de l'état. Pourquoi les objets mutables sont un modèle cassé de la réalité. - The Value of Values (2012) — Pourquoi les valeurs (des faits immutables) sont supérieures aux places (des références mutables) comme fondation des systèmes d'information. - Hammock Driven Development (2010) — Un plaidoyer pour réfléchir avant de coder. S'éloigner du clavier. Laisser l'inconscient travailler. Le meilleur débogueur, c'est un hamac. - The Language of the System (2012) — Pourquoi les propriétés d'un bon système (files d'attente, immutabilité, couplage lâche) devraient se refléter dans le langage de programmation lui-même. - Spec-ulation (2016) — Une critique acerbe du versionnage sémantique et des breaking changes. Si vous cassez vos consommateurs, vous n'avez pas versionné — vous avez créé une nouvelle chose. Chaque talk part des premiers principes. Hickey ne dit pas « voici un pattern ». Il dit « voici comment on pense ce problème — et voici pourquoi cette pensée est fausse ». Puis il propose une alternative qui, une fois entendue, est difficile à oublier. Datomic : repenser la base de données En 2012, Hickey et son équipe ont sorti Datomic, une base de données construite sur les mêmes principes que Clojure : - Des faits immutables. La base stocke des faits dans le temps. Rien n'est jamais modifié ni supprimé — de nouveaux faits sont assertés, les anciens restent. L'historique complet de vos données est toujours disponible. - Séparation des lectures et des écritures. Les requêtes ne sont pas en compétition avec les transactions. Les lectures se font sur un snapshot immutable. - Langage de requête Datalog. Au lieu de SQL, Datomic utilise un langage de requête basé sur la logique qui compose naturellement et évite le décalage d'impédance entre le code et les requêtes. Datomic n'est jamais devenu mainstream, mais ses idées ont infusé dans l'industrie. L'event sourcing, les logs append-only, les bases de données temporelles — la conversation a évolué en partie parce que Hickey a construit un système fonctionnel qui prouvait que ces idées n'étaient pas juste académiques. La philosophie en dessous Ce qui distingue Hickey de la plupart des créateurs de langages, c'est qu'il part de la philosophie, pas de la syntaxe. Ses influences — la philosophie du processus d'Alfred North Whitehead, la distinction en théorie de l'information entre information et mécanisme — ne sont pas des références typiques en programmation. Sa thèse centrale est d'une simplicité trompeuse : la plupart de la complexité logicielle est accidentelle, pas essentielle. On complique les choses non pas parce que le problème l'exige, mais parce que nos outils et nos habitudes nous poussent à entrelacer les préoccupations. Les objets mutables encouragent le couplage. L'héritage encourage la taxonomie là où il n'y en a pas. Les ORM encouragent à faire semblant que les bases de données sont des magasins d'objets. L'antidote, selon Hickey, est toujours le même : revenir aux données. Les données sont simples. Les données sont transparentes. Les données ne se cachent pas derrière des méthodes. Les données composent. Si votre architecture est construite sur des données brutes — des maps, des vecteurs, des sets — elle est plus facile à inspecter, tester, sérialiser et raisonner que n'importe quelle hiérarchie de classes. Il y a aussi chez Hickey une dimension profondément artisanale qu'on ne retrouve chez presque aucun autre créateur de langage. Pour lui, coder n'est pas assembler des briques — c'est concevoir des systèmes vivants. Un programme n'est pas un artefact figé qu'on livre et qu'on oublie : c'est un organisme qui évolue, qui accumule des faits au fil du temps, qui doit rester compréhensible à mesure qu'il grandit. C'est pour ça que l'immutabilité n'est pas un choix technique chez Hickey — c'est une éthique de conception. Chaque décision dans Clojure et Datomic reflète cette obsession de créer des systèmes qui vieillissent bien : qui restent malléables sans devenir fragiles, qui accueillent le changement sans perdre leur intégrité. Comme un ébéniste qui choisit le sens du grain avant de couper, Hickey passe plus de temps à réfléchir au problème qu'à écrire du code — et ça se voit dans chaque couture de ses créations. Pourquoi il compte Rich Hickey compte parce qu'il a prouvé que la simplicité n'est pas un compromis. On n'a pas besoin de sacrifier la puissance pour gagner en clarté. On n'a pas besoin de sacrifier la performance pour gagner en immutabilité. On n'a pas besoin de sacrifier le pragmatisme pour gagner en élégance. Il a aussi prouvé quelque chose de moins technique mais tout aussi important : on n'a pas besoin d'être bruyant pour être influent. Pas de tempêtes sur Twitter, pas de flame wars, pas de hot takes. Juste un langage, une base de données et une poignée de talks — chacun disant : réfléchis plus, entrelace moins, et fais confiance aux données. C'est suffisant. C'est plus que suffisant."
  },
  {
    "id": "resume:en",
    "href": "/exports/resume-full.html",
    "locale": "en",
    "kind": "resume",
    "title": "Full résumé — Guillaume Bailleul",
    "description": "Senior full-stack software engineer with 15 years of experience, from Go and Python backends and distributed systems to React and React Native interfaces.",
    "tags": [
      "CV",
      "résumé",
      "resume",
      "parcours",
      "experience"
    ],
    "content": "Guillaume Bailleul Senior Full-Stack Software Engineer — Go/Python, React, and Distributed Systems Lille, France · +33 7 81 79 29 34 · guillaume.bailleul@gmail.com · LinkedIn · GitHub --- Professional Summary Senior full-stack software engineer with 15 years of experience designing and shipping complete products: Go and Python backends, APIs, and distributed systems on the server side, React and React Native applications on the client side. My background spans IoT platforms, a very-high-scale email delivery API, a document management solution, a maritime AI platform, and HR products. My consistent focus has been transforming complex business requirements and existing systems into robust, scalable, and maintainable services. My full-stack profile builds on Go and Python on the server side, backed by deep Elixir experience, and on TypeScript, React, Next.js, and React Native on the client side. Accustomed to contributing from architecture through delivery in international teams and business-critical production environments, I combine long-term thinking with pragmatic execution. --- Professional Experience Senior Software Developer Andjaro | Paris, Ile-de-France, France (Remote, English) | October 2021 - April 2026 Following Andjaro's acquisition by Silae in 2023, I successively worked within two product teams. Silae HR Suite | 2024 - 2026 HRIS SaaS solution for small and mid-sized companies, covering HR processes from hiring to payroll preparation and used by more than 2,000 customers to manage approximately 230,000 employment contracts per year. - Progressively modernized critical legacy Symfony components, including Claude-assisted analysis and refactoring of design issues, while preserving production stability - Designed a Python/FastAPI agentic workflow based on LangGraph to explain payroll items, reducing the related customer support backlog by 80% - Improved the reliability of AI-assisted development through stronger tests, Claude policies, reusable skills, Model Context Protocol (MCP), and subagents - Prototyped internal DevOps tools in Go, then led technical discussions comparing Communicating Sequential Processes (CSP), Elixir's actor model, and the event loops of Node.js and Python - Supported six team members with no prior Claude experience through the adoption of generative AI despite strong initial reluctance - Designed integrations with payroll systems, ERP platforms, planning tools, and HRIS platforms Technologies: Python, FastAPI, Go (R&D), Symfony, PostgreSQL, Docker, Kubernetes, LangGraph, MCP, Claude Andjaro Platform | 2021 - 2024 Workforce management platform for healthcare, the public sector, retail, and facility management. - Developed and evolved core HR platform features in Elixir/Phoenix - Co-designed business workflows and React interfaces tailored to the French Ministry of Education - Collaborated remotely within an international, English-speaking engineering team Technologies: Elixir, Phoenix, React, Symfony, PostgreSQL, Kafka, Docker, Kubernetes Independent Software Engineer Independent (Pagily) | Lille Region, France | December 2019 - October 2021 Pagily is a document management platform dedicated to paperless workflows, universal scanning, and secure printing. - Led the technical direction, architecture decisions, and product delivery - Designed .NET/C# backend services using a modular architecture based on dependency injection, layered services, and microservice patterns for secure document processing and indexing - Developed the web application in React and Next.js, as well as the mobile application in React Native - Implemented an automated document classification and indexing pipeline - Designed integrations with multifunction printers and scanning equipment Technologies: .NET/C#, React, Next.js, React Native, PostgreSQL Software Developer Shipfix (Veson Nautical) | Paris, Ile-de-France, France (Remote, English) | April 2019 - November 2019 Shipfix was a startup developing an AI-driven platform for maritime chartering and operations. - Rapidly delivered end-to-end product features, from Python and Elixir backend services to the React Native mobile application - Developed a Python/FastAPI API for email parsing and data extraction - Designed interactive opportunity-matching workflows, collaboration tools, and real-time analytics features Technologies: Python, FastAPI, Elixir, React Native, PostgreSQL, Docker Senior Software Developer Agence Dernier Cri | Lille Region, France (Remote, English) | August 2016 - April 2019 Mailjet SendAPI v3.1 - Architected and implemented Mailjet SendAPI v3.1 in Go - Delivered on an existing GCP infrastructure a distributed API capable of handling millions of email delivery requests - Integrated Cassandra message storage with legacy Free Pascal systems Technologies: Go, PostgreSQL, Docker, GCP, Cassandra Leroy Merlin Monitoring Platform - Designed a real-time monitoring system with an Elixir/Phoenix backend and React Native application - Designed high-frequency ingestion pipelines for multiple business entities Technologies: Elixir, Phoenix, React Native, PostgreSQL, Docker Multi-Business-Unit Data Platform — ADEO - Designed an internal platform enabling each business unit to define its own data schemas through an API - Implemented data validation, consistency, and integrity controls before persistence - Designed an inter-business-unit authorization system that restricted each entity to data it had been granted permission to access - Delivered the MVP of this strategic project on schedule as part of a team assembled specifically for the project and bringing together contractors from several companies Technologies: Node.js, MongoDB Consulting Engagements - Cityscoot: architected APIs capable of handling high loads generated by mobile usage - Les Hôtels très particuliers: stabilized legacy systems, reduced technical debt, and transferred knowledge to the team Technologies: Python, Go, Node.js, React, React Native, Cassandra, PostgreSQL, Elixir Software Developer Elicity/Xee | Lille, France | December 2013 - July 2016 Xee developed an IoT platform connecting an in-car device, cloud services, APIs, and third-party applications. - Designed and developed an IFTTT-like event automation engine for the XeeCLOUD platform - Migrated data from MySQL to Cassandra and MongoDB to adapt storage to new use cases and volumes - Progressively modernized a Symfony 2 monolith into Go, Python, and Node.js microservices, including rewriting the Doctrine 2 persistence layer as a high-performance Go REST API - Architected and developed a high-traffic distributed IoT platform based on microservices and event-driven processing with RabbitMQ - Improved production reliability by maintaining the historical API, redesigning push notifications, and automating the CI/CD pipeline with Docker, Jenkins, and Ansible Technologies: Go, Python, Node.js, RabbitMQ, Cassandra, MongoDB, MySQL, Docker, Jenkins, Ansible, Symfony 2 --- Software Developer EDF | Lille, France | September 2011 - August 2013 Designed and developed a call-performance monitoring tool for EDF customer contact centers, initially deployed regionally and later rolled out nationwide. - Automated data extraction from the existing call-tracking software - Aggregated and calculated performance indicators from multiple data sources - Developed a real-time dashboard to display key performance indicators (KPIs) in customer contact centers Technologies: .NET, MySQL Education Master's degree in Computer Engineering — EFFICOM Lille | 2013 Professional Bachelor's degree in Industrial Computing and Networks (IRI) — IUT A de Lille | 2011 Technical Skills Architecture and backend: REST API design, microservices, distributed systems, event-driven processing, business integrations, and legacy system modernization. Backend languages: Go, Python, Elixir, TypeScript/Node.js, C#, and PHP. Backend frameworks: Phoenix/OTP, FastAPI, .NET, and Symfony. Web and mobile: React, React Native, TypeScript, Vue.js, and Next.js. Data and messaging: PostgreSQL, Cassandra, MongoDB, MySQL, Kafka, and RabbitMQ. Cloud and delivery: delivery of services to existing Google Cloud Platform (GCP) infrastructure, Docker, Kubernetes, CI/CD, Jenkins, and Ansible. Agentic AI: workflow design with LangGraph, MCP integrations, and AI-assisted development with Claude. --- Key Achievements - Architected and implemented Mailjet SendAPI v3.1 in Go, a distributed API handling millions of email delivery requests - Modernized the XeeCLOUD IoT platform through data migrations, progressive extraction from the Symfony monolith, and development of event-driven services - Led the architecture and delivery of Pagily, from its .NET document services to its React and React Native applications - Designed critical integrations with payroll systems, ERP platforms, planning tools, HRIS platforms, and scanning equipment - Designed an agentic workflow explaining payroll items, reducing the related support backlog by 80% --- Languages - French: Native - English: Professional working proficiency — used both verbally and in writing within international and distributed teams - Remote collaboration with English, Spanish, Ukrainian, and Russian colleagues"
  },
  {
    "id": "resume:fr",
    "href": "/exports/resume-full.fr.html",
    "locale": "fr",
    "kind": "resume",
    "title": "CV complet — Guillaume Bailleul",
    "description": "Ingénieur logiciel fullstack senior avec 15 ans d'expérience, des backends Go et Python et systèmes distribués jusqu'aux interfaces React et React Native.",
    "tags": [
      "CV",
      "résumé",
      "resume",
      "parcours",
      "experience"
    ],
    "content": "Guillaume Bailleul Ingénieur logiciel senior fullstack — Go/Python, React et systèmes distribués Lille, France · +33 7 81 79 29 34 · guillaume.bailleul@gmail.com · LinkedIn · GitHub --- Résumé professionnel Ingénieur logiciel fullstack senior avec 15 ans d'expérience dans la conception et la mise en production de produits complets : backends Go et Python, API et systèmes distribués côté serveur, applications React et React Native côté client. Mon parcours couvre des plateformes IoT, une API d'envoi d'emails à très grande échelle, une solution de gestion documentaire, une plateforme d'IA maritime et des produits RH. Mon fil conducteur est la transformation de besoins métier complexes et de systèmes existants en services robustes, évolutifs et maintenables. Mon profil fullstack s'appuie côté serveur sur Go et Python, enrichis par une expérience approfondie d'Elixir, et côté client sur TypeScript, React, Next.js et React Native. Habitué à intervenir de l'architecture à la livraison dans des équipes internationales et des environnements de production critiques, j'allie vision à long terme et exécution pragmatique. --- Expérience professionnelle Développeur logiciel senior Andjaro | Paris, Île-de-France, France (Remote, anglais) Octobre 2021 - Avril 2026 Après l'acquisition d'Andjaro par Silae en 2023, intervention successive au sein de deux équipes produit. Silae RH Suite | 2024 - 2026 Solution SIRH SaaS destinée aux PME et ETI, couvrant les processus RH de l'embauche à la préparation de la paie et utilisée par plus de 2 000 clients pour gérer environ 230 000 contrats de travail par an. - Modernisation progressive de composants Symfony legacy critiques, incluant l'analyse et la refactorisation assistées par Claude de problèmes de conception, tout en préservant la stabilité en production - Conception en Python/FastAPI d'un workflow agentique fondé sur LangGraph pour expliquer les éléments de paie, réduisant de 80 % le backlog du support client associé à ces demandes - Fiabilisation du développement assisté par IA : tests renforcés, politiques Claude, skills réutilisables, Model Context Protocol (MCP) et sous-agents - Prototypage en Go d'outils DevOps internes, puis animation de sessions comparant les Communicating Sequential Processes (CSP), le modèle d'acteurs d'Elixir et les boucles événementielles de Node.js et Python - Accompagnement de six membres de l'équipe, sans expérience préalable de Claude, jusqu'à l'adoption de l'IA générative malgré une forte réticence initiale - Conception d'intégrations avec des systèmes de paie, des ERP, des outils de planification et des SIRH Technologies : Python, FastAPI, Go (R&D), Symfony, PostgreSQL, Docker, Kubernetes, LangGraph, MCP, Claude Plateforme Andjaro | 2021 - 2024 Plateforme de gestion des effectifs destinée à la santé, au secteur public, au retail et au facility management. - Développement et évolution des fonctionnalités cœur de la plateforme RH en Elixir/Phoenix - Co-conception de workflows métier et de leurs interfaces en React spécifiques à l'Éducation nationale - Collaboration à distance au sein d'une équipe d'ingénierie internationale et anglophone Technologies : Elixir, Phoenix, React, Symfony, PostgreSQL, Kafka, Docker, Kubernetes Ingénieur logiciel indépendant Indépendant (Pagily) | Région lilloise, France Décembre 2019 - Octobre 2021 Pagily est une plateforme de GED dédiée à la dématérialisation, au scan universel et à l'impression sécurisée. - Pilotage de la direction technique, des choix d'architecture et de la livraison du produit - Conception de services backend en .NET/C# avec une architecture modulaire fondée sur l'injection de dépendances, les services en couches et des patterns microservices pour le traitement et l'indexation sécurisée de documents - Développement de l'application web en React et Next.js, ainsi que de l'application mobile en React Native - Mise en place d'une chaîne automatisée de classification et d'indexation des documents - Conception d'intégrations avec des imprimantes multifonctions et des équipements de scan Technologies : .NET/C#, React, Next.js, React Native, PostgreSQL Développeur logiciel Shipfix (Veson Nautical) | Paris, Île-de-France, France (Remote, anglais) Avril 2019 - Novembre 2019 Shipfix était une startup développant une plateforme orientée IA pour le chartering maritime et les opérations. - Livraison rapide de fonctionnalités produit de bout en bout, des services backend en Python et Elixir à l'application mobile en React Native - Développement d'une API Python/FastAPI de parsing d'emails et d'extraction de données - Conception de workflows interactifs de mise en correspondance d'opportunités, d'outils de collaboration et de fonctionnalités d'analyse en temps réel Technologies : Python, FastAPI, Elixir, React Native, PostgreSQL, Docker Développeur logiciel senior Agence Dernier Cri | Région lilloise, France (Remote, anglais) Août 2016 - Avril 2019 Mailjet SendAPI v3.1 - Architecture et implémentation de Mailjet SendAPI v3.1 en Go - Livraison sur une infrastructure GCP existante d'une API distribuée capable de traiter des millions de requêtes d'envoi d'emails - Intégration d'un stockage de messages Cassandra avec des systèmes legacy en Free Pascal Technologies : Go, PostgreSQL, Docker, GCP, Cassandra Plateforme de monitoring Leroy Merlin - Conception d'un système de monitoring temps réel avec backend Elixir/Phoenix et application React Native - Conception de pipelines d'ingestion à haute fréquence pour plusieurs entités métier Technologies : Elixir, Phoenix, React Native, PostgreSQL, Docker Plateforme de données multi-BU — ADEO - Conception d'une plateforme interne permettant à chaque business unit de définir ses propres schémas de données via une API - Mise en place de contrôles de validation, de cohérence et d'intégrité des données avant leur enregistrement - Conception d'un système d'autorisations inter-BU limitant l'accès de chaque entité aux seules données dont la consultation lui était accordée - Livraison dans les délais du MVP de ce projet stratégique, au sein d'une équipe constituée spécifiquement pour le réaliser et réunissant des prestataires de plusieurs entreprises Technologies : Node.js, MongoDB Missions de conseil - Cityscoot : architecture d'API capables de supporter de fortes charges liées aux usages mobiles - Les Hôtels très particuliers : stabilisation de systèmes legacy, réduction de la dette technique et transfert de connaissances à l'équipe Technologies : Python, Go, Node.js, React, React Native, Cassandra, PostgreSQL, Elixir Développeur logiciel Elicity/Xee | Lille, France Décembre 2013 - Juillet 2016 Xee développait une plateforme IoT reliant un boîtier automobile, des services cloud, des API et des applications tierces. - Conception et développement d'un moteur d'automatisation événementielle de type IFTTT pour la plateforme XeeCLOUD - Migration des données de MySQL vers Cassandra et MongoDB afin d'adapter le stockage aux nouveaux usages et volumes - Modernisation progressive d'un monolithe Symfony 2 vers des microservices en Go, Python et Node.js, incluant la réécriture de la couche de persistance Doctrine 2 sous la forme d'une API REST Go haute performance - Architecture et développement d'une plateforme IoT distribuée à fort trafic, fondée sur des microservices et des traitements événementiels avec RabbitMQ - Fiabilisation de la production par la maintenance de l'API historique, la refonte des notifications push et l'automatisation du pipeline CI/CD avec Docker, Jenkins et Ansible Technologies : Go, Python, Node.js, RabbitMQ, Cassandra, MongoDB, MySQL, Docker, Jenkins, Ansible, Symfony 2 --- Développeur logiciel EDF | Lille, France Septembre 2011 - Août 2013 Conception et développement d'un outil de suivi des performances d'appels pour les centres de relation client d'EDF, d'abord déployé à l'échelle régionale puis généralisé à l'échelle nationale. - Automatisation de l'extraction des données depuis le logiciel de suivi d'appels existant - Agrégation et calcul des indicateurs de performance à partir de sources de données multiples - Développement d'un tableau de bord temps réel pour visualiser les indicateurs clés de performance (KPI) dans les centres de relation client Technologies : .NET, MySQL Formation Master en ingénierie informatique — EFFICOM Lille | 2013 Licence professionnelle Informatique et réseaux industriels (IRI) — IUT A de Lille | 2011 Compétences techniques Architecture et backend : conception d'API REST, microservices, systèmes distribués, traitement événementiel, intégrations métier et modernisation de systèmes legacy. Langages backend : Go, Python, Elixir, TypeScript/Node.js, C# et PHP. Frameworks backend : Phoenix/OTP, FastAPI, .NET et Symfony. Web et mobile : React, React Native, TypeScript, Vue.js et Next.js. Données et messaging : PostgreSQL, Cassandra, MongoDB, MySQL, Kafka et RabbitMQ. Cloud et déploiement : livraison de services sur des infrastructures Google Cloud Platform (GCP) existantes, Docker, Kubernetes, CI/CD, Jenkins et Ansible. IA agentique : conception de workflows avec LangGraph, intégrations via MCP et assistance au développement avec Claude. --- Réalisations clés - Architecture et implémentation en Go de Mailjet SendAPI v3.1, une API distribuée traitant des millions de requêtes d'envoi d'emails - Modernisation de la plateforme IoT XeeCLOUD : migration de données, extraction progressive du monolithe Symfony et développement de services événementiels - Pilotage de l'architecture et de la livraison de Pagily, de ses services documentaires .NET à ses applications React et React Native - Conception d'intégrations critiques avec des systèmes de paie, des ERP, des outils de planification, des SIRH et des équipements de numérisation - Conception d'un workflow agentique d'explication des éléments de paie, avec une réduction de 80 % du backlog de support associé --- Langues - Français : langue maternelle - Anglais : niveau professionnel — utilisé à l'oral et à l'écrit dans des équipes internationales et distribuées - Collaboration à distance avec des collègues anglais, espagnols, ukrainiens et russes"
  }
]