Simple is Better Than Complex

Simple is Better Than Complex

My other job is being a partner in a bootstrapped SaaS business. Our product is a web app that brings GitHub-style visibility to Excel workbooks.

Our users push their Excel workbooks to a GitHub repository. We mirror that GitHub repository and let our users browse and diff their workbook's content as if it wasn't a binary file.

At its core, the technical challenges are all about parsing Excel files (binaries) and serialising them into a bunch of Git-friendly text and JSON files.

Depending on the size and complexity of the Excel workbook, this is a slow task. Multiple Excel files per commit, multiple commits per push and multiple repositories only add to the problem.

We rely heavily on Celery for this. As a Git commit comes in, we offload the workload to a Celery task. The Celery task traverses the commit in search of any Excel files and parses them. It then rewrites the commit with the serialised Excel file content, replacing the original binary file.

The root of all problems

In the early days of our product, I was fixated on optimising the time to process a commit. And so I deployed every trick in the Celery book. Parallelisation was my answer to everything.

The version we shipped to our first paying on-premise customer back in 2017 relied heavily on chains, groups and chords. It was fast. And it was complex.

It was so horribly complex that troubleshooting was literally finding that needle in a haystack.

My premature fixation on efficient processing not only led to a complex Celery workflow canvas. It also led to some design decisions that proved difficult to migrate away from as a bootstrapped startup with limited manpower.

Simplify all the things

Fast forward to 2023 (!) and through a combination of utter frustration and fortunate circumstances, we finally decided to reduce complexity and refactor. It took my business partner and me six months to tame the beast.

Our codebase now weighs 13,000 lines of code less. What used to be a complex Celery workflow, made up of over ten tasks, is now a simple single task. Task progress is now easy to follow, troubleshoot and feedback to the user via the UI.

When something goes wrong, I have a pretty good idea of where to start looking. What used to cost me hours (not including the time I procrastinated because of the pure thought of the horror that was to come), is now down to ten minutes most of the time.

Does processing an Excel file now take longer? Yes, it does. But as it turns out, a good UI experience makes more than up for it.

Lessons learned

Here are my lessons learned, even though they all sound like clichés:

  • stick with a simple Celery workflow canvas

  • avoid groups and chords if you can

  • if you think you cannot avoid groups and chords, think again

  • beware of "design spills" when using complex Celery primitives

  • decide on your app workflow first, then on your Celery implementation

PS: Our old Vue.js frontend is gone as well. We replaced it with htmx, but that's another story for another day.

Did you find this article valuable?

Support Bjoern Stiel by becoming a sponsor. Any amount is appreciated!