Every team has that one internal tool that started as a lark. At pistach.top, our quality control pipeline used to be a patchwork of shell scripts and Python one-liners, glued together with cron jobs. Then a developer built something in Go over a long weekend. Three years later, that side project handles over 90% of our automated inspections. This is the story of how it happened, what almost went wrong, and what we learned about turning a side project into production infrastructure.
Where the Side Project Came From
Our quality control workflow had a familiar problem: we needed to validate thousands of product listings every day—checking image dimensions, metadata completeness, and formatting consistency. The existing solution was a collection of Bash scripts that each team member maintained independently. They worked, but barely. A single malformed CSV could cascade into hours of debugging.
One of our engineers, frustrated with yet another Friday-night data fix, started writing a small tool in Go. The initial scope was narrow: parse a JSON config file, run a series of checks, and output a structured report. No database, no web server, no fancy architecture. Just a binary that took input and produced output. The whole thing was maybe 500 lines of code.
Within a month, three other teams were using that binary. They didn't ask permission; they just downloaded the compiled executable and pointed it at their data. That's when we realized we had something bigger than a weekend hack.
Why Go Worked for This Problem
Go's simplicity was a double-edged sword. It made the initial code easy to write and debug, but it also meant the developer couldn't easily add complex abstractions later. The lack of a framework forced a straightforward design: read config, run checks, write output. That simplicity turned out to be a feature, not a limitation.
We also benefited from Go's static binaries. Teams could drop the tool onto any server without worrying about dependencies. No Python version conflicts, no missing libraries. That portability was the main reason the project spread so quickly.
Foundations That Confuse Most Teams
When we talk about this story at conferences, people often ask, "How did you convince management to support a side project?" The honest answer is: we didn't. The project grew organically because it solved a real pain point. But that organic growth created its own problems.
First, there was no documentation. The original developer had a mental model of how the tool worked, but no one else did. When he went on vacation, the rest of us had to reverse-engineer the code to fix a bug. That was a wake-up call.
Second, the tool had no tests. Not because the developer was sloppy, but because it started as a prototype. By the time it became critical infrastructure, adding tests felt like a massive undertaking. We had to freeze features for two sprints just to get coverage above 60%.
The Configuration Trap
Another common mistake was treating the config file as an afterthought. Early versions used a flat JSON structure with no schema validation. A single typo in a field name would cause the tool to silently skip an entire category of checks. We lost a week of inspection data before we realized the bug.
We eventually adopted a strict schema with versioning. Every config file now includes a version field, and the tool rejects any file that doesn't match the expected schema. It felt heavy-handed at first, but it saved us from countless silent failures.
Missing Observability
The original binary printed results to stdout. That was fine for a single run, but when the tool started running in CI pipelines and scheduled jobs, we had no way to track performance or error rates. We added structured logging and metrics output, which let us build dashboards. That visibility was the turning point—it turned a black box into a manageable system.
Patterns That Usually Work
Through trial and error, we identified several patterns that made the tool reliable and maintainable. These aren't Go-specific; they apply to any side project that's growing into production.
Strict Input Validation
We learned to validate inputs early and fail fast. The tool now checks every config file for structural correctness before running any inspections. If a field is missing or malformed, the tool reports the exact line and suggests a fix. This reduced support requests by 70%.
Plugin Architecture for Checks
Instead of hardcoding every inspection rule, we designed a plugin system. Each check is a separate Go package that implements a simple interface. New checks can be added without modifying the core binary. This made the tool extensible without becoming a monolith.
The plugin approach also made testing easier. Each check can be tested in isolation, and the core binary only needs integration tests for the plugin loading mechanism.
Graceful Degradation
Not all failures are equal. We categorized checks into critical and advisory. A critical failure stops the pipeline; an advisory failure generates a warning but allows processing to continue. This distinction prevented small issues from blocking the entire quality control process.
We also added a retry mechanism for transient failures, like network timeouts when fetching reference data. The tool retries up to three times with exponential backoff before reporting a failure.
Anti-Patterns and Why Teams Revert
Not every decision we made was a good one. Some patterns we tried ended up causing more harm than good.
Over-Abstraction Too Early
After the tool gained popularity, we tried to refactor it into a generic "inspection engine" that could handle any type of data. We added layers of abstraction—interfaces for data sources, result handlers, and configuration providers. The result was a codebase that was harder to understand and no more flexible than the original.
We eventually reverted most of those abstractions. The lesson: let the tool evolve to meet actual needs, not imagined future ones.
Ignoring Performance Until It's Too Late
The original tool processed a few hundred items per run. As usage grew, runs started taking hours. We had ignored performance because it wasn't a problem in the early days. When it became a problem, we had to rewrite the core loop to use parallel workers.
We should have added concurrency from the start, or at least designed the data flow to be parallelizable. The rewrite took two weeks and introduced bugs that took a month to stabilize.
Letting the Side Project Become a Black Box
Because the tool was a side project, the original developer made all the decisions. When he left the company, we had a knowledge gap that took months to fill. We now require that any tool used in production has at least two people who understand its architecture.
Maintenance, Drift, and Long-Term Costs
Maintaining a tool that started as a side project comes with hidden costs. The most obvious is documentation debt—the original developer's mental model is often the only source of truth. But there are subtler costs.
Dependency Drift
Go modules helped, but we still faced issues when third-party libraries updated their APIs. The tool depended on a YAML parser that broke after a major version bump. We had to pin dependencies and set up automated update checks.
We now run a weekly job that checks for outdated dependencies and opens pull requests. The team reviews and merges them during low-traffic periods.
Cultural Resistance to Change
Teams that adopted the tool early became attached to its quirks. When we tried to change the output format, we faced pushback from users who had built scripts around the old format. We had to support both formats for six months before deprecating the old one.
This taught us to version the output format from the start. Any breaking change should be a new version, with a clear migration path.
The Cost of Not Rewriting
At some point, every side project faces a decision: rewrite or keep patching. We chose to keep patching for two years, which made the codebase increasingly fragile. Eventually, we did a partial rewrite of the most critical components. That rewrite was smaller than a full rewrite but still required significant effort.
Our advice: don't be afraid to rewrite parts that are causing pain. A targeted rewrite is often cheaper than living with the pain or doing a full rewrite.
When Not to Use This Approach
Not every side project should become a production workhorse. We've seen cases where the approach failed, and it's worth knowing the warning signs.
When the Problem Is Better Solved by a Commercial Tool
If the problem is generic (e.g., log aggregation, monitoring), a commercial tool or open-source project is likely better maintained and documented. Building your own makes sense only when your requirements are unique or you need tight integration with your stack.
In our case, we evaluated several quality control platforms. None of them handled our specific data format or allowed the level of customization we needed. That justified building our own.
When the Team Lacks Long-Term Ownership
A side project that becomes critical infrastructure needs a dedicated owner. If the original developer is the only person who understands it, and they're likely to leave, the risk is high. We almost lost the tool when our lead engineer left. We were lucky that another team member had been gradually learning the codebase.
If you can't commit to at least two people understanding the system, it's better to use a more standard solution.
When the Requirements Change Too Fast
Side projects work best for stable, well-understood problems. If your quality control requirements change every month, a custom tool will become a maintenance nightmare. In that case, a more flexible platform or a set of scripts might be better.
We've seen teams try to build generic platforms that never stabilize. The result is a half-finished tool that no one wants to use.
Open Questions and FAQ
We often get asked about specific aspects of the journey. Here are the most common questions, with honest answers.
How did you get buy-in from other teams?
We didn't ask for buy-in. We made the tool available and let teams adopt it on their own. Once a few teams saw results, word spread. The best marketing for an internal tool is a solved problem.
Did you ever consider rewriting in another language?
Yes, we considered Rust and Python. Go stayed because it was already working, and the cost of rewriting outweighed the benefits. The tool's performance was adequate, and the team was most productive in Go.
How do you handle security?
The tool runs in a sandboxed environment with no network access to production data. It reads from a read-only copy of the database. We also run regular security scans on the codebase.
What would you do differently?
We would add tests and documentation earlier. We would also design the output format to be versioned from day one. And we would have a conversation about long-term ownership before the tool becomes critical.
Summary and Next Experiments
The journey from shell script to production workhorse taught us that side projects can be powerful, but they require intentional care as they grow. The key is to recognize when a prototype has outgrown its original purpose and invest in it accordingly.
Here are three specific actions you can take this week:
- Audit your internal tools. Which ones are used by more than one team? Those are candidates for formal support.
- Add a version field to any configuration file that your tools consume. It's a small change that prevents big headaches.
- Schedule a knowledge-sharing session where the original developer walks two other team members through the codebase. Even an hour of walkthrough reduces bus factor.
Next, we're experimenting with a plugin marketplace where teams can share custom checks. We're also exploring a web UI for non-technical users to trigger inspections without touching the command line. The side project that started as a weekend hack is now the foundation of our quality control strategy—and it's still evolving.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!