“If it’s working, don’t touch it.”
That’s one of the most repeated sayings in software engineering.
I’ve never been very good at following it.
I recently went back to an MVP I built nearly 2 years ago.
It was live. It worked. Users were using it.
But as I read through the code again, it was obvious that:
the architecture could be cleaner
the file structure could be more intuitive
scaling this further would become painful
So I refactored it — and ended up rewriting over 60% of the system.
Key changes I made
State management
I replaced the Context API (which had grown messy at the app root) with Zustand.
The result was a simpler mental model and better separation of concerns.
Chat flow architecture
I moved from a linked-list approach to a stack-based system, paired with a pointer that tracks the current chat step.
This made it possible to:
retained persistence of chat history
rehydrate conversations accurately
allow users to resume chats instead of restarting them
The UX improvement here was significant.
What didn’t go smoothly
I experimented with XState to manage chat steps and history.
While powerful, it quickly became too verbose for this specific use case and risked becoming a long-term maintenance problem.
I also ran into serialization issues.
Some chat messages contained React components, which can’t be safely serialized and deserialized while retaining behavior.
The solution was to store intent-based representations instead of components, and resolve them at render time.
Outcome of the refactor
faster load times and chat responses
accurate chat rehydration
cleaner and more intuitive file structure
= improved API security and auditing
Refactoring a live MVP isn’t flashy, but this is the kind of work I enjoy:
making systems easier to evolve, safer to extend, and ready for scale.
Still early, but intentionally building this for long-term use.