Code, Context, and Growth

A reflection on my coding journey over the past 8 years, and what it means to write code in different organisational contexts.

@caifanuncle

May 2025

1. The Scrappy Startup CTO

Being the CTO of your own startup basically means you're the only developer until you can afford to hire someone.

You're searching for product market fit, and product velocity is top priority. Code quality is secondary, especially when you're the only developer. Beautiful code only shines when development becomes a team effort.

The core backend was an express server. The frontend was a single page app built with React. The mobile client was a Flutter app. The database was MongoDB. MERN was the "in" thing back then. We tapped onto Firebase for authentication and realtime features. Queries got complex. We introduced caching with Redis. We learned along the way. It was like building a plane while flying it.

No tests, no sophisticated CI/CD pipelines, pushed straight to master and tested on production.

But it worked. We had 10k+ users. The internals might have been a mess, but the product looked beautiful (IMO).

I continued shipping garbage despite thinking I was good at my craft. I did not know what I did not know.

2. Freelancer for traditional SME (Part I)

I left the startup I built. A salon chain asked me to build a loyalty and booking app for their business. I said ok.

Firebase for authentication, realtime database, storage, and serverless functions. Flutter for the mobile app. React for the admin dashboard. Slightly more experienced at building working software, I managed to ship relatively fast. However, UI, database and business logic were still tightly coupled at the code level due to lack of experience. This made changing business rules and the eventual migration from serverless more cumbersome later on.

3. SWE @ Scale-Up

Opened my eyes to the world of software engineering at the enterprise level.

Two main sources of complexity:
1. Software architecture was based on distributed monoliths, with each smaller service managed by a team of not less than 5 people.
2. Business rules that govern loyalty programs for financial services are highly arbitrary and sophisticated in nature.

Complexity has to be managed, and also compressed.

Managing Complexity:
We implemented rigorous processes like RFCs (Request For Comments) for architectural changes and stringent code reviews to maintain quality. Our sophisticated CI/CD pipelines automated testing and deployment, while comprehensive unit and E2E tests ensured reliability. We prioritized backward compatibility to prevent breaking changes, designed APIs with idempotency to handle duplicate requests gracefully, and implemented robust log tracing for debugging. Maintaining data consistency across distributed systems was a constant challenge that required careful design and monitoring. We quickly adapted to new business rules and requirements by adopting layered abstractions in our code (eg. actions, calculations and data). The list of considerations goes on and on, but you get the point.

Compressing Complexity:
Choice of programming language can play a huge role in compressing business domain logic. Ruby's expressive syntax allowed us to create elegant domain-specific abstractions that closely mirrored business concepts. Ruby is no longer as popular as it used to be, but I'm so glad that I got to work with it.

Despite huge technical growth, software development no longer became a creative endeavour for me. As a software engineer, I was not involved in product strategy and design. Perhaps rightfully so, but that's what I used to love about building software.

4. Freelancer for traditional SME (Part II)

Working at a proper tech company made me realise how shitty the code that had been running in production since my university days was. I decided to do a rewrite.

I've gotten better at writing abstractions and decoupling layers. I've also developed my own flavour and opinions on how to build software, and an appreciation for the different nuances between technical choices.

I migrated the serverless backend to a Ruby on Rails backend, which also served a React frontend with InertiaJS as the glue. Data migration from Firestore to Postgres was less painful than expected with code gen. Moving data from Firestore to Postgres also unlocked massive opportunities for data analysis.

I'm very happy with the current state of the codebase.

The app has facilitated 40k+ bookings for 15k+ customers since its humble Firebase beginnings.

5. Tech for Public Good

Products built for public good often do not share the same scaling requirements as enterprise products. Business logic tends to be straightforward and self-contained, with little cross-cutting concerns between different services.

However, not every app has to be built with enterprise in mind in order to be worthwhile; not every non-enterprise application is a toy app. A simple CRUD app, when done well and applied in the right context, can solve very real and complex problems. That is most important.

That said, complexity in public sector apps manifests in infrastructure and data management requirements. Intranet, on-prem, internet, blablabla. I'm still trying to make sense of it all.

Working in smaller product squad teams, I also get to be more involved in the product strategy and design process. This resonates with my personal belief that app developers should be more product-oriented.