If you've read me before, you know where I stand: most of our job is learning how things work while we keep complexity under control. This post is a concrete example of how I've been designing and building things lately, trying to keep complexity as low as I can and the cost of each solution small. The idea is simple. Every decision that can be postponed gets postponed, until the moment we actually need to build it.
In Turning up the dial I described the workspace where I do this kind of work, and promised to take it apart piece by piece. Consider this the first piece. Everything in this post happened in there, from the skills I used to the task file where the postponed decisions still live. That post was about the system. This one follows a single feature through it, from the first question to the first real users.
The interesting part isn't the idea. It's the flow, the tools, and the list of decisions we didn't take.
Why this matters more now
AI has removed a lot of friction from building. It also helps a bit with understanding code and systems we don't know; I can get a decent map of an unfamiliar repo in a few minutes. But the amount of complexity a team can hold in its head, operate, debug at three in the morning and change without fear is about the same as it was. Maybe a little more with better tools. Not much more.
We are still the bottleneck. Every streaming connection, every subscription model, every cache adds to the basal cost of a system, the part you keep paying whether anyone uses the feature or not. When writing code was slow, that cost hid behind the effort. Now the code appears quickly, and the complexity stays with us.
So the useful question has changed. It's no longer "can we build it?". It's "what's the least we can build to find out if it deserves more?".
The case
We're removing Slack as the place where our platform talks to people. Alarms, the progress of long operations (creating an application, for example) and personal notices go through a small notification service that, until now, delivered everything to Slack. Some colleagues, who joined from another company, don't use Slack at all. When they create an application from our CLI they get "submitted"... and then nothing.
The feature: let people read their notifications from the CLI.
One more piece of context, because it changes how you should read every number below. I don't code full time. Between meetings, people and everything else, development gets maybe 10–15% of my focus on a good day. So when I say something took a day and a half, that's calendar time at my pace, in the gaps. Someone working on this with full focus would have needed a fraction of that.
This is what the whole thing looked like, from the first question to feedback from real users:
Step 1. Diverge, on purpose
I started a Claude Code session with the grilling skill (from Matt Pocock's skills, I keep a copy in my skill-factory repo). It interviews you about a plan, one question at a time, each with options and a recommendation, until nothing important is left implicit. I asked for exactly that format: one question, a few options, I pick or I write my own answer.
25 questions in 21 minutes.
And here's the part I'm not proud of. Claude's first recommendations were the narrow ones: store only operation feedback, address only people. I was the one who widened the scope. "Everything". "Person, team or topic". Then I threw in an analogy ("how does kubectl do it for events?") and the design grew a server-side stream. Twenty minutes later I had a beautiful system on the table: streaming like kubectl get -w, topics people could subscribe to, read state per message plus a watermark, alarm grouping, an unread counter cached in the background, a neutral message format with a new Slack renderer on top.
None of it was unreasonable. With an agent writing most of the code, I could probably have built all of it in a couple of weeks. That's exactly the problem.
I don't think the divergence was a mistake, though. You need to see the whole problem before you can decide what to leave out. The mistake would have been to stop there.
Step 2. Converge, also on purpose
So I switched tools. I asked for two skills together: complexity-review and hamburger-method. My prompt was, more or less, "find which points add a lot of complexity at the start, how we could solve them, and a much simpler approach".
complexity-review is a skill I wrote that checks a design against 30 dimensions of complexity (state, consistency, latency, dependencies, security, lifespan...) and pushes for the simplest option that works. It found eight drivers in my design. What made it useful wasn't the list. It was the shape of each row: the complex thing, a simpler option, and when to add it.
Streaming became polling every three seconds, "until polling load or latency is a measured problem". Grouping got postponed with a number next to it: around 1,300 alerts in four months is about 11 a day, so "add it when one recipient gets more than ~50 a day". Topics would wait "until a real group needs a channel that is not a team". It also gave a rough guess of the ongoing cost: 15–20% of the team's capacity for my design, 3–5% for the simple one. Those are Claude's estimates, not measurements. I didn't need them to be precise. The order of magnitude was enough.
The hamburger method is Gojko Adzic's technique for slicing a feature vertically; my skill is just a way to apply it systematically. You split the feature into the steps it goes through (the layers of the burger), list several options for each layer from "almost nothing" to "the full thing", and then pick one option per layer so that every slice works end to end. Here it was five layers with five options each:
Slice 1 is the green path. Store every message the service already receives in one Postgres table. Show people their own messages and their teams'. Reuse the existing API key plus an identity token. A plain list command in the CLI. No read state at all; if you want recent stuff, use --since. Estimated at 2–3 days.
Claude proposed slice 1 plus three more cards for the next slices. I went further: one card, slice 1 only, "and write down under what conditions we'd fill in each of the initial decisions, if that time ever comes".
That sentence is the heart of this post.
Step 3. Write down the triggers
The ideas from the divergent phase didn't go to the bin. They went to a table in the task: the postponed decision, the design we had already agreed on in case we build it, and the trigger that tells us it's time. By the end of the work the table had sixteen rows. Here are some of them, with what happened next:
| Postponed | Slice 1 does instead | Trigger to build it | What happened |
|---|---|---|---|
| Server streaming | List only | Polling load or latency is a measured problem | Waiting |
| Live follow by polling | List only | Someone needs to watch messages live | Fired on day 2 |
| Identity-only route | API key + identity token | The shared key must stop reaching clients | Fired on day 3 |
| Read state (one watermark per person) | None, filter with --since |
Daily use makes "what's new" hard to see | Waiting |
| Alarm grouping | One row per delivery | One recipient gets > ~50 a day (measured then: ~11) | Waiting |
| Topics | Unrouted messages go to the owning team | A group needs a channel that isn't a team | Waiting |
| Map GitLab users to directory ids | Store the id as it comes, log a warning | The warnings show a real gap | Gap measured: 107 of 138 GitLab messages go to a bot |
| Retention (90 days) | Keep everything | ~1M rows, or before general availability | Waiting |
Writing this table changed how converging felt. Cutting scope usually feels like losing something. Here nothing was lost: the design was there, agreed and written down, waiting for a reason. It also makes the conversation with yourself (or with your team) much shorter the day someone says "we should add topics". Fine. Has the trigger happened?
Step 4. Make the change easy
Before building the slice, I asked Claude to look at the current architecture of the service and prepare it for the feature, using two more skills: thin-wrappers (wrap each piece of infrastructure behind a small interface in the language of the domain) and hexagonal-architecture. It found five problems. The most telling one: the ports were shaped like Slack. Routing only knew Slack targets. If I had added the inbox on top of that, the inbox would have inherited Slack's concepts forever.
That turned into six preparation steps with no new behaviour: domain ports, the GitLab webhook going through the same path as every other message, a shared Postgres base, migrations. About a day of work.
It's the old Kent Beck line: "for each desired change, make the change easy (warning: this may be hard), then make the easy change". I'll be honest, this is also where the method gets a bit uncomfortable. That day was design work done before the slice existed, which is precisely what the rest of this post tells you to avoid. My take is that it's a different kind of design: it doesn't add capabilities, it removes friction from the next change. But I can see someone arguing the other way.
Step 5. Make the easy change
Then the slice itself, in small safe steps (yes, there's a skill for that too). 35 commits in the service and 7 in the CLI between the 19th and the 21st, every push going through the pipeline to production. The database code went to production inert first, with nothing calling it; then the database declaration; then the wiring; then the recording, live on the morning of the 20th. With mutation testing on the pieces that mattered: 46 mutants on the component that records messages, all killed, and two of them found real gaps before production did.
Slice 1 was storing real messages about 38 hours after the design session, and readable from the CLI that same evening. Inside the 2–3 day estimate, and that's with a small slice of my day. With full focus it would have been hours, not days. I think that's the part that surprised me most: the thin slice is so small that even part-time it reaches production before the week is over.
Step 6. Let production decide what's next
Two triggers fired in the first 72 hours.
The first was live follow. I wanted to watch my notifications while an operation ran, so polling moved from the table into the same task as a second slice. The trigger wasn't a metric; it was a user asking. That user was me. I'm fine with that, but it's worth saying.
The second one was more interesting. For the colleagues without Slack, the CLI couldn't depend on a shared API key they'd never have. That was exactly the trigger we had written: "the shared key must stop reaching clients". Before opening the new route we measured what it would cost to abuse it (every forged token meant one request to the identity provider: 50 fake tokens, 50 requests) and added a limit. The design was already in the table. Building it took a morning, a part-time one.
Then came real users. Seven engineers without Slack created test applications with the CLI open in another terminal, following their notifications. One wrote "all good, it took around 30s for the first message to appear". Another: "within a few seconds". Each run produced about eight messages, addressed to the right person.
And real use found things no design session would have found. With no Slack account, the service fell back to a shared Slack channel, so about 37 of their messages also landed in a place nobody expected. One path stored messages for a username that wasn't a person in our directory, so nobody could ever read them. And 107 of 138 GitLab messages were addressed to a bot. That last one was already a row in the table. Now it has a number.
What I'm less sure about
The feedback came from a guided beta that lasted a couple of days, not from people using this every morning. A week later I have no data showing daily use. The check "you see your messages and nobody else sees them" is proven by the data, not by a second person actually trying. And none of the other triggers have fired. Maybe the slice is enough. Maybe nobody uses it enough to need more. I don't know yet, and I'd rather say it.
I'm also aware that the design ended up simple because I asked for it to be simple, after letting it grow. The skills didn't decide anything. They made it cheap to open the space and then cheap to close it again. The table made closing it feel less like losing.
So
Diverge to see the whole problem. Converge to the thinnest slice that works end to end. Don't throw away what you saw: write each postponed decision next to the trigger that brings it back, with a number measured today if you can. Make the change easy, then make it. And let production tell you which row comes next.
None of this is new. Vertical slicing and the last responsible moment are old ideas. What's new is the pressure. When an agent can turn the whole divergent design into working code by Friday, the discipline to converge is about the only thing between us and a system bigger than we can understand.
Which leaves me with a question: now that building the full version costs almost nothing, how many of the features we ship will be just a divergent phase that nobody bothered to converge?
Related reading
- Turning up the dial: seeing the forest while working on any tree, the workspace and way of working this flow lives in
- Encoding Experience into AI Skills
- Gojko Adzic, Splitting user stories: the hamburger method
- My skills: eferro/skill-factory (complexity-review, hamburger-method, small-safe-steps, thin-wrappers, hexagonal-architecture, mutation-testing)
- Matt Pocock's skills, where grilling comes from
- Kent Beck, "make the change easy, then make the easy change"
- The Art of Small Steps in Software Development: A Lean Vision








