đ§© Introduction
Building the best milkman on Earth requires thinking fast. You come up with a bold idea, trial it with a PoC, convince the stakeholders â and bam! Your service is now helping hundreds of people across dozens of warehouses decide where to put each product.
Thatâs how the original Slotting Service (SLS) came to life: a system designed to assist with making thousands of tiny decisions every day, all of which add up to something huge. We figure out where each product should live within a warehouse so that warehouse workers can grab it faster, restock it smarter, and move through their shifts with fewer steps and less hassle. Whether itâs placing fast-moving items close to the outbound gates or keeping heavy items at waist height to avoid injuries, every decision is made with purpose. Itâs all about speed, safety, and efficiency â and SLS became the brain behind it.
As the years passed, the service began to show its age. It started to resemble an old man â once sharp and nimble, now slower, fragile, and set in his ways. It groaned under pressure when more data came in. A simple code change could throw it off balance. It was no longer fit to keep up with the demands of a rapidly evolving warehouse landscape. For example, implementing a new capacity calculation strategy for a subset of products required careful tweaks within a thousand lines of code module consisting of all potential options mixed together.
Developers began to feel the burden. Investigating a bug meant navigating a maze of assumptions, legacy logic, and unclear boundaries. The service had become not only fragile but frustrating.

Finally, the team made its case, and leadership agreed: it was time for a new service. Hooray! Pop the champagne!
But after the celebration, a sobering question emerged:
Weâre building a new service. Now what?
In this post, weâll walk through how we approached designing the new Warehouse Slotting Service (WSLS): the decisions we made, the traps we avoided (or fell into), and what we learned along the way.

đ The Design Spiral
Designing WSLS wasnât straightforward. Initially we believed that it would be a simple task. After all, we had a working slotting service already. This was just a cleaner, more flexible version, right?
That illusion lasted about a week.
đŠ When the Domain Fought Back
We tried modeling it the straightforward way: define core entities (Article, Location, Assignment, etc.), draw some arrows, and clean it up in post.
But nothing stayed clean.
Every concept blurred into three others. For instance:
- A Candidate Location wasnât a real location â it was a possible future, maybe dependent on other futures.
- A Task wasnât a work unit â it was a container for trees of dependency-ordered moves.
- The âsameâ warehouse had multiple simultaneous truths: what planners believed, what the main Warehouse Management System held, and what a human walking the aisles would see.
We werenât designing a system. We were negotiating ontologies.
Each time we thought weâd pinned things down, new edge cases or cross-cutting concerns would emerge. To make progress, we started drafting proposals â rough cuts of what individual services might look like, how they might relate, and where boundaries could sit. Each draft clarified some parts while raising new questions about ownership, state, and sequencing. The more we iterated, the more we realized how much we didnât know yet.
So we changed tactics.

đŁ The Birdhouse Breakthrough
When regular meetings stopped being helpful, we literally left the building. If the problem was stuck in our heads, maybe the solution needed a change of air. We took a day off from the usual rhythm and drove out to a quiet spot somewhere in the countryside near Amsterdam. The place was modern enough, with big windows and clean whiteboards, but the air conditioning picked the worst possible day to give up. By mid-morning, it felt more like a greenhouse than a meeting room. Still, it had what we needed: four walls, a bit of quiet, and no distractions.
We split into pairs and revisited the services weâd loosely sketched in earlier sessions. This time, each pair took ownership of a single service. It wasnât a pure blank state. We carried baggage from earlier brainstorms â assumptions, mental models, rough ideas â and poured them out onto the tabula rasa of each service. The goal was to extend our knowledge of the services and understand where their boundaries really lay. It was structured brainstorming with permission to forget just enough to see things differently.

Somewhere between the heat, the whiteboards, and a steady flow of scribbled diagrams, something clicked. The mess of overlapping concerns began to settle. Concepts that had felt blurry in isolation made more sense when framed by neighboring responsibilities. By the end of the day, we had a rough MVP of the service design and, more importantly, a shared mental map of how the system held together. It was the first time the whole thing felt buildable.
đ€ Defining the Edges
Once the fog started to lift, we still werenât ready to build. We had emerging concepts and cleaner boundaries, but it was time to test whether they held up under pressure â not in theory, but through actual flows.
So we started working the system.
We mapped out core user journeys like loading slotting, making a slotting move, canceling changes, submitting assignments, and starting a task. If a concept didnât show up clearly in a diagram, it probably wasnât ready. If two services needed the same data for different reasons, we had to ask why and who should really own it.

We created sequence diagrams for each flow, reviewing and iterating on them until the handoffs felt clean. We defined each componentâs role in terms of behavior and data: what it did, what it owned, and how it talked to others. We turned these into RFCs and refined them in parallel, constantly checking for friction at the seams.
From this, the responsibilities crystallized.
The Assigner service mastered the slotting states. It handled assignments from the moment of creation to execution on the floor. It provided structure and sequence, making the rest of the system feel grounded.
The Validator service enforced the rules like âdonât put glass products too highâ or âdonât tear down packages if we can pick from them. It ran checks for strict correctness, soft warnings, and hard violations. This was a huge leap from the old service, where such rules lived in scattered corners, if at all. Now they shared a proper home, and we could flexibly toggle specific checks per warehouse or adjust their configuration with ease. If Assigner was the backbone, Validator was the gut check.
The Tasker service managed the choreography. It translated the updates from state to state and propagated them to downstream systems.
These services didnât emerge fully formed. They were shaped by the flows they needed to support. A concept like âsubmit slottingâ wasnât just a button in the UI; it carried a trail of meaning behind it. It meant validation had to be strict, transitions atomic, and task generation downstream-safe â no edge cases left to chance.
We could finally trace a full journey from user intent to system action to physical movement. But even then, something still felt tentative. Our diagrams were cleaner. Our boundaries were tighter. But did the model actually work?
âïž When the Model Clicked
There was a moment when things finally began to click. Not all at once â more like puzzle pieces locking into place, one at a time. The flows made sense. The data models werenât fighting us anymore. The roles of each service had become coherent, and we could talk about system behavior without running into mismatched assumptions every ten minutes.
The clearest signal? We could finally fully embrace this layered architecture diagram:

Each layer in it â from Angular frontend to SQL state â represented a set of contracts we could finally define, separating our concerns:
- The FE team knew exactly what data to expect, and from where.
- The API surface had stabilized enough to be documented and versioned.
- Each route mapped cleanly to one or more service responsibilities.
- Core services and resource owners could be implemented independently and tested in isolation.
- Repositories respected the shape of the data models instead of working around them.
- And the state layer no longer had to carry the burden of business logic.
Once this emerged, we could finally dig into writing real contracts: endpoints, input/output schemas, edge case handling, and test expectations. And perhaps more importantly, we could explain the system â to each other, to new joiners, and to stakeholders.
This was the moment we shifted from modeling reality to building with confidence.
đ ïž Tips, Tricks, and Pitfalls: What Worked and What Didnât
Every project teaches you something. WSLS taught us a lot, sometimes the hard way. Here are the lessons weâd pass on to any team building a new service in a complex domain.
Know your users. All of them.
Our first rollout at a warehouse looked like a success. No errors, no complaints, and nothing unexpected in the logs during working hours â a perfect launch, at least on paper. But then came the evening shift.
In the old setup, 95% of usage came from product slotters â the planners working during the day. Operators on the evening shift simply interacted with the output, usually outside of the old system. In WSLS that changed: a key step in the process to preview and accept instructions before pushing changes to the floor moved inside the system. This fell directly under the warehouse operators scope, and we hadnât accounted for them.


The oversight cost us more than a few bug fixes. We needed to escalate support, patch workflows on the fly, and rethink how we structured our release planning. What we took away from that experience was simple but lasting: understanding your core user group isnât enough. You need to know the full cast, especially those whose use cases arenât visible in your daily test cycle.
Brainstorms are gold. Until they go stale.
One of the most productive days on this project wasnât spent coding or debugging. It was spent away from our desks â laptops open, diagrams flying, deep in a birdhouse offsite.
That session let us question our assumptions, explore the unknowns, and sketch out the actual shape of the system with just enough structure to stay focused, and few enough distractions to actually think. It was a breakthrough. Compare that to our earlier pattern of weekly brainstorms, which slowly lost focus and energy over time. The same topics kept resurfacing, the same ideas kept getting rehashed, and forward momentum gave way to fatigue.
We realized that the best brainstorms arenât just about time or frequency, but timing and intent. Used well, they unlock clarity. Used routinely, they create noise. Brainstorms are like seasoning â powerful, but best used sparingly or under the hand of a great cook.
Think big. Build small.
We started with bold ideas: guided workflows on handheld scanners, a rule engine that planners could tweak on the fly, and more. These werenât far-fetched â just a clear picture of where the system could eventually go. But good systems donât start that way.
An MVP should prove the value, not showcase the vision. Ours used matplotlib to render instructions on how to implement the changes to assignments on the floor. This was a direct copy paste from the old service. It was painfully slow, not fit for scale, but enough to test core assumptions. That restraint paid off: six months later, with real data and confidence, we swapped in an HTML renderer that was 24x faster without breaking anything.
Ambition is healthy. But build small first, so your system can grow safely when the time comes.
Define contracts. Not just code.
At Picnic, we lean heavily on component tests. They are fast, focused, and work well with our pace of iteration. We use the human-readable format of Pythonâs behave framework, which not only speeds up development but also makes tests accessible to non-engineers. At least in theory.
In practice, we often chose to move fast rather than spend time with users aligning on expectations up front. Tests were written after the fact, more as safety nets than shared agreements. If we had involved stakeholders earlier and used tests as contracts, we mightâve spotted critical flow gaps sooner, like the one we missed during the warehouse rollout.
We also donât have full end-to-end tests for the flow, and that was a deliberate choice. E2E tests come with significant complexity and cost, and not every system needs them. Observability, manual QA, and strong component test coverage carried us far. That said, as our system becomes more interconnected, weâre beginning to see this as a valuable piece of tech debt worth addressing.
Weâre learning to treat tests not just as technical checks, but as tools for communication. That shift isnât about writing more tests. Itâs about writing the right ones, at the right time, with the right people.
Define your language.

Itâs not about choosing Python over Java, or vice versa. Itâs about aligning terminology.
A âshelf-ready articleâ can mean one thing to a developer, another to a warehouse planner, and something else entirely to a supply chain analyst, if thereâs no shared vocabulary in place. We learned that the hard way, repeatedly misunderstanding each other despite good intentions and detailed specs.
One of the biggest improvements over the old service is that we can now âtraceâ the full lifecycle of assigning a product to a location: from a draft created by a planner, through a target awaiting implementation, to a factual physical placement. We went through 69 versions of a glossary page where we, among other things, tried to define âassignmentsâ and its states as unambiguously as possible. Painful? Absolutely. But necessary. That single word touched nearly every component in the system, and without a shared definition, we were building software for three different realities.

Agreeing on language didnât just clean up documentation. It changed how we structured our models, how we tested our rules, and even how we explained failures. Sharing a language turned into sharing ownership â and thatâs when things really started to work.
Perfect is the enemy of good.
We know this one. We say it a lot. But weâre still learning how to live it.
Thereâs a temptation to get things ârightâ the first time. That pressure makes it easy to delay delivery in favor of âjust one more improvement,â and to overlook debt that builds up beneath the surface. The challenge isnât just technical, itâs cultural. The more success we have, the harder it becomes to justify shipping something rough around the edges, even when itâs safe to do so.
Weâve found ourselves struggling with tech debt lately. It piles up quietly, often disguised as small decisions or shortcuts taken in good faith. What we lack is a reliable way to measure it, something that helps us see when the cost of change is creeping up or where complexity is starting to calcify. Without that, itâs hard to have honest conversations about when to invest in cleanup, and when itâs okay to let it ride.
So weâre working on it. Not just to make things perfect â but to make it safe not to.
Do not optimize too early.
We didnât just learn this late. Weâre still learning it now.
In one of our early versions, we introduced a cache to make the system feel faster. It worked, sort of. But the moment we needed to evolve the logic underneath, we realized how tightly we had bound ourselves to a shortcut that was meant to be temporary. What started as a performance win quickly became a maintenance trap.
Looking back, the cache wasnât the problem â the timing was. We added it before we had a full picture of how the system would be used, before the logic was stable, and before we had proper instrumentation to tell if performance was actually an issue. It was a case of building for imagined scale, not real needs.
The result was avoidable complexity, and it made future iterations harder than they needed to be. Now, we remind ourselves: if your MVP needs a cache to function, thatâs not a performance problem â itâs a design problem. And optimization should follow understanding, not precede it.
Measure what matters. Early.
One of the best ways to avoid getting surprised by your system is to make it tell you how itâs doing, before someone else does.

We learned that the hard way. We shipped a seemingly harmless improvement to our logging, which quietly turned into a flood. The system started slowing down. But no alerts went off. Instead, users started reporting lag. Then came a message from our observability team: weâd blown through our entire monthly log budget in just a few hours. Turns out, the PR had turned our log stream into a firehose, and it overloaded both our system and our logging infrastructure.
Thatâs the thing about observability: itâs not just a nice-to-have, itâs your smoke detector. The earlier you wire up good alerts, the more you can steer with confidence, both technically and operationally. Are assignments being generated at the right rate? Are suboptimal placements decreasing over time? Is performance degrading silently under load? These arenât questions you want to answer when the fire starts.
Donât wait until your users notice. Let your system speak up first.
đ Conclusion
Building a new service isnât just about writing code. Itâs about reshaping workflows, making implicit knowledge explicit, and getting a group of people to see the same picture at the same time.
WSLS taught us that thoughtful design pays off, but only if youâre willing to stay flexible when reality shows up with a plot twist. The glossary drafts, system diagrams, and early flow mappings gave us a solid starting point. The MVP constraints (and the times we broke them) helped keep us focused.
It also wouldnât have been possible without the solid platform foundations we stood on. The ability to spin up a Python service quickly, provision infrastructure with confidence, and rely on stable tooling meant we could focus on the hard stuff â the domain, the flows, the humans.
Rolling out WSLS was a coordinated dance: multiple warehouses, different systems, staggered testing, lots of Slack channels. But it worked. And even though itâs âjust a service,â it now sits at the heart of a daily operation â helping real people work more safely and efficiently.
The biggest impact, though, may be what itâs unlocked.
In a fast-paced environment like Picnicâs â especially within the warehouse domain, where new types of warehouses and fulfillment models are constantly being explored (like our Automated Fulfilment Center) â iteration speed is everything. With WSLS in place, weâve gained a stable foundation to try new ideas faster. Whether itâs testing alternative placement logic, refining workflows, or scaling to new formats, weâre no longer rebuilding the basics each time. Weâre iterating at the business level and not just the technical one.
In the end, every service is a mini-startup. You need a vision, a team, a plan, and a launch. Success didnât come from nailing it up front. It came from building something that could adapt, improve, and grow â just like the warehouses it serves.
*Approximately 30% of the early sketches made it to production, which I would consider a strong outcome.
From Fragile to Flexible: Rebuilding Our Slotting Brain from Scratch was originally published in Picnic Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.