tl;dr flows that are fundamental to the product should be written with flexibility in mind
As a software engineer at an early stage startup, I have seen the constant push and pull between shipping faster and writing higher quality, flexible code. While this topic is subjective and will differ between startups and team, I do think that the mantra of “move fast and break things” is often used blindly and at the expense of code extensibility.
early stage startups must iterate fast
Its not very visible unless you’re in the ecosystem, but your favourite products were very different at inception. Twitch started as Justin.tv in 2007 for lifestyle streams and only switched focus to gaming in 2011. Slack was born from an internal communication tool used by Tiny Speck, a gaming company that no longer exists.
They have reached this stage through numerous changes (though often sticking to a fundamental problem they want to solve) based on user feedback, market reception, internal motivations and often a pivot or two. Successful startups that have pivoted products dramatically early on talk extensively on the need for velocity. The bias is towards quick and dirty code rather than over-engineering, since so many initial ideas and prototypes fizzle out.
Code is
extensible
orflexible
when you can easily add or extend current functionality without having to go back and rewrite the existing code. Minimizing touching existing code minimizes bugs.
when to optimize for extensibility
While iterating fast applies to testing nascent ideas, I have realised certain core flows fundamental to a startup’s product (or pivot or new direction) merits more upfront design for flexibility.
Spending that extra day or two in shipping a feature or making an architecture decision can pay off in dividends. On the other hand, over-engineering can be expensive (opportunity cost and maintainability).
Here are a couple identifiers I use for flows that require focus on flexibility:
- They are tied in with long term goals of the product
- They are slated for frequent feature extensions
- They are tied into multiple product flows
effective extensibility
In most cases, extensibility can be introduced by following simple design principles. It’s tempting for engineers to use “extensibility” as an excuse to experiment with trendy new frameworks or patterns. Over-engineering components without justified business value often leads to technical debt and messy systems down the roads. If a truly complicated extensible solution is required, the opportunity will naturally show itself as the product and business needs evolve.
When working on the notifications systems at Locale.ai, I identified and deliberately spent a few extra days to map out the problem and its future scope. I knew this was a fundamental flow (keeping ops in the loop) and will need to be added on in the future (we started with just emails and Slack).
I designed an event-driven architecture and modelled the objects keeping dynamic templating and multiple notification channels in mind. I separated the templating logic from messaging, the templating was handled by the query service which would then populate the right fields for the notifications service to send out the complete message (separation of concerns). This enabled rapid iteration and we are able to add new notification channels in a fraction of the time and with better developer experience.
I often compare this to the test notification flow we put together at around the same time. It was written quickly without much planning to allow users to preview notifications. However, this resulted in technical debt. For every new notification channel added since, it has been simple to set up the new channel in the core notification flow but it was cumbersome to create the test flow.
I have used orange to indicate changes in
variables
and blue forchannels
. Notice how they match up in the core flow but are all over the place in the test flow.Adding new channels to the core notification flow only required adding templates in the notifications service and the variables were handled implicitly through our resolver utility in the query service.
However, the test notification flow was tightly coupled between the client and the service, with channels and variables defined in the API contracts. Adding a channel meant creating new endpoints or modifying existing ones with conditional switches. Modifying templates was painful as it required changing the variables in the API contracts, which often cause backwards compatibility issues for users on older client versions.
when to refactor
A good opportunity to refactor old code (and introduce flexibility) arises when another developer revisits it. Following the simple principle of “leaving it better than found” is straightforward to implement and gives great results. Retroactively revisiting code that has gained usage is another good way.
At Locale.ai, we had a query executor service which was built on the assumption that most users will connect relational databases to the product, which held true for a while. But as adoption grew, we noticed that connecting data sources often required users to set up bastion hosts or loop in their DBA which increased conversion times by a big margin. We decided to introduce APIs and Google Sheet connectors to open up the product to more data sources and make it easier for business folks to connect (Excel and Google sheets is what most are comfortable with).
We identified this as the right moment to clean up the mess and make the query service more extensible. Doing this at the right time, enabled us to build a multi-source enrichment feature into the product and enable certain ETL sources (Amazon selling partner, Shopify) in no time.
making the case
When proposing investing extra time upfront in extensible code, demonstrate how it ultimately boosts velocity. For example, our notifications engine’s flexibility allowed adding channels in days, keeping users happy. Good developer experience and cleaner code also attract and retain quality engineers.
Focus on extensibility where it unlocks business value - like core flows slated for frequent extension. Make a strong case to the right stakeholders and don’t overapply the principle. With balance, extensibility unlocks big wins.