
The Cost of Solving Problems You Don't Have
TL;DR: Complex technology isn't necessarily bad. Complexity without a problem to justify it is. On one product, we spent roughly a week introducing GraphQL and teaching the team how to work with it, without ever getting much value from it. On the same product, a decision-tree architecture solved a problem we encountered almost every day. Both added complexity. Only one had a problem big enough to justify it.
Around 2020, I worked on a new B2B product for a large company in the fragrance industry.
COVID had made an important part of their sales process suddenly very difficult.
Traditionally, salespeople would visit potential customers in person and conduct what is essentially an olfactory briefing. They would bring fragrance samples, ask questions, discuss what the customer was trying to create and gradually develop an understanding of the desired scent.
At the end of that process, a contract could be signed and the requested samples would be produced.
Getting those samples to the customer could take six to eight weeks.
Our job was to digitize that process.
It eventually became much more than putting a questionnaire on a website. The digital product was connected to SAP and to a newly established production facility where mixing and sending samples could be automated.
The result was that customers could receive samples in roughly one to two weeks instead of six to eight.
It was a fascinating product to work on.
But before we got very far into solving that problem, we spent about a week solving another one.
A problem we didn't really have.
We started with GraphQL
The product was greenfield.
We had a Spring Boot backend and a Nuxt frontend, with three backend and three frontend engineers as part of a much larger cross-functional team.
Our CTO wanted to use GraphQL.
I wasn't convinced that we needed it, but the decision was made and we started implementing it.
That meant getting GraphQL working properly in Spring Boot, setting up Apollo on the frontend, figuring out server-side rendering and establishing the conventions the team would use.
It also meant learning.
Most of the team hadn't worked extensively with GraphQL before, so we weren't only introducing a technology. We were introducing a new way for frontend and backend to communicate.
I remember us spending roughly a week or two experimenting until we were happy with the setup.
Then we started building the actual product.
And over the course of the project, I never really saw the return on that investment.
GraphQL wasn't the problem
I don't think the lesson is that GraphQL is bad.
It solves real problems.
If you're operating a platform with many different clients, different data requirements, independent teams or a public API used in ways you don't fully control, those problems can become very relevant.
We didn't have them.
We had a web application.
Maybe there could eventually have been another client or two, but we weren't building an API platform for hundreds of unknown consumers.
Some of the other advantages also turned out to be less relevant in practice than they sounded in theory.
Type safety between frontend and backend was useful, but GraphQL wasn't the only way to get it. OpenAPI and generated clients can solve that problem as well.
The ability to evolve an API without breaking clients sounds great too.
But that requires discipline.
If nobody wants to maintain deprecated fields indefinitely, fields eventually get renamed or removed anyway. In our case, the theoretical advantage didn't fundamentally change how API evolution happened.
And SSR with Apollo introduced additional work of its own.
None of these things made GraphQL bad.
They just made me ask a question I should have weighted more heavily at the beginning:
What problem are we actually solving with this?
Then we encountered a problem we definitely had
The core of the product was the olfactory briefing.
Describing a fragrance digitally is surprisingly interesting.
You can't simply ask someone:
What should it smell like?
The briefing had to gradually narrow down what the customer wanted.
What kind of product are they creating?
Is it a home hygiene product?
Body wash?
A candle?
What feelings should the fragrance evoke?
Images could be used to explore associations.
Colors could help narrow down a direction.
Words could describe characteristics that are difficult to express through traditional product attributes.
And depending on previous answers, completely different questions could become relevant.
This wasn't a form.
It was a flow.
And that flow changed constantly.
The product changed almost every day
The briefing was the core of the product, so Product, Design and our domain experts were continuously learning.
A question would move.
A branch would change.
A new path would become necessary.
A question that made sense for one type of product wouldn't make sense for another.
Several previous answers might determine what should happen next.
During development, changes like these happened almost every day.
If the flow had been tightly coupled to the UI and application logic, every product experiment would have become an engineering task.
Fortunately, I had encountered a similar problem before.
I had seen this problem in a completely different industry
A few years earlier, I had worked on call-center software for an appliance company.
The software guided agents through conversations.
What should the agent ask next?
Which question becomes relevant depending on previous answers?
Which paths can be skipped?
Where do different paths merge again?
That problem had led me to a concept commonly used in game development:
Decision trees.
When we started working on the olfactory briefing, I realized that despite the completely different domain, the underlying problem looked remarkably similar.
So we used the same idea.
Instead of implementing the briefing directly in the presentation layer, we described its structure separately.
The application could then interpret that structure and render the appropriate experience.
The definition of the flow and the presentation of the flow became separate concerns.
The complexity had somewhere to go
A decision tree isn't necessarily simpler than a normal form.
That wasn't the goal.
The difference was that its complexity corresponded to complexity that actually existed in the product.
The briefing genuinely had branches.
Questions genuinely depended on other questions.
Paths genuinely needed to merge again.
Questions genuinely needed to be skipped.
And those relationships genuinely changed all the time.
We weren't designing for a hypothetical future in which the briefing might one day become dynamic.
It was already dynamic.
Almost every day.
So we gave that complexity a structure.
That changed what experimentation looked like.
Instead of rewriting parts of the application every time the briefing changed, we could change the definition of the flow.
Questions could move.
Branches could change.
New paths could be introduced.
The UI didn't have to be reinvented every time the product team learned something.
That was complexity worth paying for.
The same idea eventually became Quaire
Interestingly, I didn't immediately turn the first decision-tree implementation into a generic library.
I had used the concept once in the call-center application.
Then I used it again for the olfactory briefing.
Only after seeing the same underlying problem in two very different products did it become obvious how generally useful the pattern could be.
Surveys have this problem.
Onboarding flows have this problem.
Questionnaires have this problem.
Guided configuration can have this problem.
So I eventually extracted the concept into an open-source library called Quaire.
Quaire separates the data describing a questionnaire from its behaviour and presentation.
A flow can be linear:
A → B → C → D
It can branch:
→ B → C
A → → F
→ D → E
Questions can be skipped.
Paths can loop.
Different branches can merge again.
Questions can depend on answers given elsewhere in the flow.
The interesting part isn't that these things are technically sophisticated.
The interesting part is that every one of them came from behaviour I had actually needed.
Compare those two decisions
Looking back at the project, I find the contrast useful.
We introduced GraphQL before building much of the product.
It was a sophisticated solution with real capabilities.
But most of those capabilities weren't responses to problems our product was experiencing.
Then we introduced a decision-tree architecture.
That also added abstraction.
It also added concepts an engineer had to understand.
It also made the architecture more sophisticated than simply putting a series of form fields into a component.
But this time we could point directly at the problem.
We had encountered it yesterday.
And there was a good chance we'd encounter it again tomorrow.
Both decisions added complexity.
Only one had a problem big enough to justify it.
Complexity isn't the enemy
I like simple software.
I generally prefer boring technologies, established patterns and as little code as possible.
But "keep it simple" can become dogmatic too.
Sometimes the domain is complicated.
Sometimes the product is complicated.
Sometimes a sophisticated abstraction is exactly what makes the rest of the system simpler.
The question isn't:
Is this architecture complex?
The better question is:
What existing complexity does this architecture remove or organize?
The decision tree organized complexity that already existed in the product.
GraphQL, for us, mostly introduced capabilities for complexity we might have had someday.
That's a very different trade.
Be suspicious of "we might eventually"
A lot of architecture starts with completely reasonable sentences.
We might eventually have multiple clients.
We might eventually need microservices.
We might eventually need to replace the database.
We might eventually have millions of users.
We might eventually expose a public API.
Any of those things could happen.
But architecture has a cost today.
Every abstraction has to be understood.
Every technology has to be learned.
Every additional layer has to be maintained.
Every capability introduces another way the system can behave.
And the future rarely arrives exactly as we imagined it.
That doesn't mean ignoring experience.
If I'm building an e-commerce product and I already know organic search will be critical, I'm not going to pretend SEO is an unknowable future requirement.
There is a difference between experience and speculation.
The difficult part is learning to recognize it.
Build for what you know
If I started that fragrance product again tomorrow, one of my first technical decisions would be very simple.
I wouldn't introduce GraphQL.
I'd start with the product.
I'd spend that first week understanding the domain, working with the fragrance experts and learning more about the briefing we were trying to digitize.
When the complicated problems appeared, we'd solve them.
Some of those solutions might be simple.
Some might require sophisticated architecture.
That's fine.
I'm not interested in minimizing complexity at all costs.
I'm interested in making sure complexity has earned its place.
Before introducing a technology, pattern or abstraction, I try to ask a much simpler question now:
What problem are we solving?
If the answer starts with:
We might eventually...
I become suspicious.
If the answer is:
This happened again yesterday.
I'm listening.
This is how I build Vuesion
These aren't just ideas I write about. They're principles I've been building into Vuesion for more than eight years.