Architecting Web Startups - making the right decisions with limitations
Who should read this?
This article is aimed for CTOs / Technical Co-Founders, Software Architects and Software Developers with prior architecture experience who are building a web product from the ground up in an early-stage start-up venture.
I assume you and your team have already validated your product at this stage by taking a prototype in front of your potential customers and are on the way to commercialise your concept. If you haven’t done that yet, you have bigger concerns than architecture such as validating your assumptions and mitigating your market risks.
Having said that, there are situations where a prototype won’t help much and you need to spend money & time to build a simple product to start conversations with your customers. Some enterprise applications are a good example of this. If you belong to this group, please read on.
What should we expect from this article?
This article is a guide on tailoring our existing technical know-how to fit it into the start-up mentality. There are some practical advices on fine-tuning our mindset, eliminating over-engineering and laser-focusing our resources to achieve the best possible architectural outcome for an early-stage company. We will dig into some of the popular architectural patterns and how we can utilise them without exposing ourselves to their radioactivity.
There are no detailed information on the technical topics discussed here. If you haven’t done any automated testing so far or don’t know what Microservices are or haven’t heard of Domain Driven Design yet, there are plenty of useful materials on the internet to have a basic understanding of these concepts in a reasonable time-frame.
Advices shared here may or may not be suitable for mature start-ups. I will advise to sacrifice some of the best practises due to limited resources. Take them with a grain of salt if you’d like to apply any of these principles to your Series C startup.
1. Mental Concerns
Getting our mindset right.
Mission-oriented: In the early stages, it is crucial to be laser-focused on our mission all the time when there are millions of things to do. Our mission is to reach product market fit. The ability to quickly evaluate what technical decisions contribute to our mission is a matter of experience in building startups. We will get better at this along the way and will continuously learn from our mistakes. When we decide on a major self initiative that is not driven by the product, we should always discuss it with the others to get an objective view on its ROI and its priority.
Business-first: Software is always an important element of the whole picture but it is not the only one. There are sales, marketing, support and other departments that are fundamental to the success of the business. The truth doesn’t change even if we are creating high-tech products. Embracing this belief will help us to avoid diverging from the path like many start-up founders with engineer backgrounds do. Our software has to support our business, not the other way around. Listen to others, leave the communication channels open and collaborate, collaborate, collaborate.
Principle-focused: It helps to work out a few core architectural principles that will guide us when we cannot make decisions or lose our focus. Software is an endless pit and going down the rabbit hole will cause us to lose the most valuable asset; time. To stay on the track, we should refer back to these principles and ask whether our decisions are aligned. These principles could be generic or contextual depending on our circumstances. Example:
- Generic example: Architecture has to increase the speed of delivery (pretty much applies to all early stage start-ups)
- Contextual example: Architecture has to enable scaling up the team quickly (lets say our sales forecasts, market conditions and competition show that we will need to double up the team in the next 12 months)
Future ready: Our architecture should always address our current state but it should also be aligned with our future vision. Every architecture and technology decision is changeable (and will change). However we would like to do it with minimal impact and make sure that the transition is as seamless as possible when the right time comes. To make our product future-ready without going down the rabbit hole, we should sit and think about where we are expecting to be in 6 months, 12 months, 24 months and 36 months. Think about this as making financial projections before you raise an investment round. It is just a projection, however it will force you to think about your product from a wider perspective.
Avoiding new risks and eliminating the existing ones.
Remember that startups are inherently risky ventures and part of our job is to reduce this risk by taking the necessary precautions.
-If we dive into Microservices head-on as a first-timer, we are increasing our technology risk by introducing new unknowns and unnecessary complexity in our life. It takes years for companies to get it right with plenty of lessons learned along the way.
-If we decide to use a new framework right after its first stable release, we are taking risk.
-If we decide to go all in on a new technology trend with a small community behind it, we are taking risk.
Our goal should be eliminating the existing risks from the risk pool, not introducing new ones. Approach this with an angel investor or a VC mindset with proper risk analysis for all your choices.
Aligning our architecture with the business targets.
Our startup’s most vital target is to make money and a monolith app will be good enough to generate reasonable annual turnover in most cases. I have been involved in start-ups making more than $20M with big monolithic applications and there are many others generating way higher revenues. Having an imaginary correlation between architecture complexity and revenue is quite common and is an unhealthy path that would lead to over-engineering. This correlation could be correct after a certain number in particular contexts but that bar is still really high.
If we have the market potential to reach and employ tens or hundreds of developers, Microservices will help us to scale these dev teams efficiently. It will place solid physical boundaries to software delivery with smaller autonomous teams, increase ownership, distribute the domain knowledge across the organisation, etc... If our market tells us that we can only gain 2 more enterprise clients within the next 10 years and an additional $1M on the horizon, our Microservices ambitions will just be engineering delusions for a while. Microservices does not only offer scalability, however before making a large-scale investment into it, we would want to make sure that we are in a position to fully utilise its benefits. Otherwise the opportunity cost comes into play. Time spent on building Microservices might as well be used on perfecting our support processes or other important dimensions.
Being aware of our unique architectural concerns.
Every product has a unique set of architectural challenges. SAAS applications, marketplaces, enterprise applications, mobile apps, desktop apps, micro web apps, web apps that are global from day one… They all have different priorities when it comes to architecture.
For example, if we are building an enterprise SAAS that will serve to the local market, some of our unique architectural concerns would be;
- How to design multi-tenancy. (DB per customer, shared DB, app per customer, shared app)
- How to cater for tens of different workflows for each customer, gracefully handling customisation requirements without regressing existing workflows and keeping maintainability at peak.
- How to deploy hot-fixes or urgent changes independently to each customer.
- How to design API integrations so that our consumers would not be effected from each other’s updates. (API versioning, Consumer Driven Contracts…)
If we are building a global SAAS-enabled marketplace, we might have additional concerns about;
- Integrating two code-bases seamlessly to maximise the business potential.
- Enabling both products to evolve separately from each other.
- Internationalisation and localisation
We cannot cover all these specific requirements here and will instead focus on more high-level, product agnostic ideas. However it is important to take these concerns into account when making decisions.
2. Architectural Concerns
Monolith, SOA, Microservices, Serverless…
Let’s be honest, most if not all of us would like to pick the most kick-ass, up-to-date and hyped architecture for whatever we are going to build. And that is an almost irresistible engineering temptation. After all, how can we brag about our engineering skills in networking events if we build up our app as a boring monolith system that was invented before Soviet Union times.
Modern architectural patterns are very useful if applied properly. They provide a high level of decomposition, effortless horizontal scalability, high cohesion, multi tech stack choice and many more advantages. On the other hand, they all come with the drawback of increased complexity in our daily jobs.
As mentioned before, we can change the direction of our architecture at any point, but having been through many transformations, I can confidently say that they are not fun. It typically takes more than a year to execute a big-scale transformation and 1 year is equal to 5 years in start-up time dimension. Therefore, it is better to set the fundamentals right from the beginning. When the codebase gets out of control, refactoring have bigger footprints and things will get nasty.
Our strategy in this topic will be building things as monolith first with Microservices in mind. This may feel like we have 2 feet on 2 horses but concepts like DDD and Macroservices (see the below sections for a further explanation on this) will help us to bridge the gap between both models.
So why are we saying “Microservices in mind” in such a context-agnostic way? It ties back to the above discussion of our business targets. In this case, we are assuming that our start-up will be big enough at some point to justify the need for the most capable, proven and universally accepted architectural strategy.
Horses before carts; getting our monolith right as a start.
Introducing a distributed system too early will get in the way of our progress. Especially if we don’t have prior experience, shifting our mindset will be an unnecessary challenge for us.
Initially we should spend most of our effort on establishing a well-structured monolith having all its layers defined properly with strict boundaries.There are many teams with talented architects who cannot get the rules and guidelines right on separating web application layers; application services, domain services, entities, repositories, infrastructure services, controllers, dtos etc... We should set the fundamentals absolutely right and nail our monolith if we’d like to take our architecture to the next level.
Keeping our layers clean and separated throughout our journey has paramount significance. We should work out guidelines on using our layers and try to follow them as much as we can so that no logic would leak into another layer and no inconsistency is introduced. This is where DDD comes to play. It helps us overcome all the challenges and ambiguities that are brought by introducing multiple layers to our application. It provides great guidance on the responsibilities of different layers and prevents to reinvent the wheel trying to solve already solved problems. (more on DDD in the next section)
If possible, we can create a pretty lightweight code review process for the initial few months to make sure our devs are aligned with the idea of where to write code. We could perhaps remove this code review process for our early stage startup once we are confident a dev has enough capabilities & training. (Again one of those ideas that may not be applicable to structured teams)
Once sorted, documenting the responsibility of each layer with good and bad examples will be very helpful moving forward. Each layer should distinguish clearly from the others in terms of responsibilities leaving no question marks. We should come back and update the document for every use-case we come across during our development.
DDD, laying the groundwork for Microservices.
First things first, let’s briefly describe the terms macro, mini and micro in the services context. Just as Microservices were gaining more and more traction, companies started adopting different approaches to implementing it as they realised that practicality varies for different contexts. Some shied away from breaking down their app into too many fine-grained services and some adapted a purist approach for granularity. This resulted as different variations of the same mentality; Macroservices, Miniservices and Microservices.
For our strategy, I am going to suggest Domain Driven Design to be applied on our monolith because its building blocks (like bounded-contexts and domain events) will help us get ready for a potential implementation of Macroservices in the future. It also brings together the best and most structured ideas so far for designing a monolith application in my honest opinion.
Why Macroservies? One of the core pillars of DDD is the bounded-context concept. It enables us to draw logical boundaries (subdomains) in our domain and differentiate / customise our entity meanings for each logical boundary. These bounded-context are not as granular and they are better represented by Macroservices. An example for a Microservice could be an InvoiceService whereas an example for a Macroservice could be a BillingService which includes Invoicing, Receipts, Taxation, Customers etc..Bounded-context will help us to draw lines for our Macroservices first. These Macroservices could then be transitioned into Miniservices and then eventually Microservices based on our future requirements.
At the end of the day, this categorisation shouldn’t mislead or confuse us and we don’t have to align ourselves with these terms. All we have to do is to adjust the service granularity as per our own requirements. From this point on, I will refer to the strategy as Microservices to keep things simple.
In a typical Microservices architecture, each subdomain data are stored in their respective databases. Multiple DB concept will however increase our infrastructure complexity in the early stages. An alternative to this is creating separate DB schemas in a single DB to represent each bounded-context that enables us to reuse table names multiple times and allow them to evolve in their own directions while still enjoying the simplicity of working on one DB. These schemas will facilitate our DB architecture transition for micro-services. We have our DB monolith ready to be broken-down into obvious multiple DBs since our constraints are established explicitly with solid fences around them.
Because we are building a monolith, our sub-domains will not be represented by separate physical services. Instead, this separation will be at the class library level. Each sub-domain will be represented by one (or multiple) class library and the interactions between each other will be through dll references.
We still need to define the methodology for the service-to-service interaction. There are two options here; orchestration and choreography. My pick here is choreography (reactive approach) over orchestration. We would not only decouple our sub-domains better, but also practise the event-driven architecture for Microservices (which is also a pillar stone for the serverless architecture). Some people shy away from using events mostly due to unfamiliarity or the traceability problems of the call stack (in the same process). However events are beasts when it comes to design flexibility and getting used to working with them is a great future investment that will level up your product capabilities in the long run.
There are 2 other DDD concepts I would like to talk about. They are not directly related to the Microservices transition but they would have different implications on start-ups vs established companies.
First one is anemic models vs rich domain models. Latter is superior and gives you much more flexibility however it is more prone to be misused by the dev team especially if they consist of junior / mid-level developers. I have also witnessed examples of senior programmers getting confused on how and why they should be dividing the business logic between entities and services. If your team nature is able to mitigate these risks, I would suggest to go with rich models, otherwise anemic model will reduce the potential risk of slowing down your development.
Second one is patterns like CQRS and Event-Sourcing. These patterns bring unique advantages and are still utilised by modern frameworks like Redux, however be cautious about introducing these kind of concepts (especially on a wide scale) at the beginning if you do not have a rock-star team. I have found many mid-senior developers to be struggling with the complexity of these and similar patterns.
Waiting for the right time to gear-up for Microservices.
We should wait for the right time to make the full transition to a complete Microservices model. This could be when our product is bringing considerable revenue, when we can attract good talent that could work with the complexity of Microservices, when we establish somewhat a proper DevOps culture, when we are not under enormous workload, when we start hiring more aggressively and have genuine reasons for splitting up to multiple teams.
We can raise extra-funding to achieve this with a well presented business case to our board or investors and justify the benefits. Series A or Series B might be a good time to gear up depending on our projections and growth.
Sprinkling a little bit of Serverless on top.
Cloud providers are heavily investing in Serverless technologies and I can see great value in adopting them (especially FAAS) as we can cut significant time from building infrastructure. They would even bring more value at the prototype building stage; however we won’t delve into it within the scope of this article.
The key idea here is to not apply these to your core domains but to benefit from them on our supporting domains or infrastructure services in the early stages. Relying on Serverless for all of our mission critical functionalities may lead to unforeseen consequences. I would be cautious here and follow a similar conservative approach that we planned for Microservices.
When it comes to where we can use them, a combination of Azure Functions (or AWS Lambda) and Azure Durable Functions could give us off the shelf support for executing our short & long running on-demand tasks without having to worry about investing in the set up of all the peripheral concerns that come with a service and their ongoing maintenance effort.
At the time of writing this article, Azure was giving away 1 million free executions per month. Know that they are priced on ram x time so make your calculations for present and foreseeable future. There is no guarantee that the price will not increase when there is more adoption, so put a buffer on that as well. Microsoft was giving away amazing BizSpark benefits to product start-ups to increase Azure adoption at first but they started cutting down on that considerably after a while.
Lastly, we should be careful about the disadvantages that come together with the Serverless approach. FAAS cold-starts could be detrimental to our business if we have time sensitive operations. (E.g. Fintech space)
What about SOA?
I think of SOA as an interim architecture state between monolith and Microservices from a start-up point of view. As we know, Microservices has been introduced to overcome the disadvantages of SOA and has evolved further over time.
Obviously there are circumstances where SOA would be more practical and useful compared to the monolith and Microservices as the software is not always black and white. However I find it unnecessary to communicate them extensively in our limited scope here.
I will be excluding SOA from this article and leave it up to you to determine it based on your own circumstances. This article would still give you ideas if you decide to go down that path.
I think most people would agree on the benefits of using a JS framework like React, Angular or Vue JS since the alternative ways of scaling up at the front-end code-base is really tedious. There is also massive community support behind the new frameworks now with major tech players using it.
My personal experience revolves around both Angular & React. I have no hard rules here on using one over the other. If you are already familiar with one of them, you should not pay the cost of switching to the other since you will always have a learning curve no matter how good you are on picking up things. It is better to spend your energy on another area.
If you are new to the JS frameworks, there are plenty of comparison articles on the internet. Weigh up the pros and cons and maybe even spend a few days on doing hands on coding to get a feel for a variety of frameworks. An easy way to do this is to complete the tutorials on codeacademy where it mandatory to write code for every single thing we learn.
It is beneficial to plug-in a state-management library(Redux, NgRx, etc..) right from the beginning as I can guarantee that things will get messy down the track. This way, we wouldn’t have to rewrite the state-management from scratch once we realise our front-end is turning into a spaghetti. Some people find state-management libraries confusing but if you have exposure to CQRS principles, you shouldn’t have a hard-time on grasping the fundamentals.
When it comes to responsiveness, a popular library like Bootstrap will help us gain tremendous speed in designing our web pages mobile & tablet friendly without spending time on digging into media queries. We would need a responsive website in most cases, however some enterprise web app users would not really use any devices other than their desktop to access our website. An upfront user research would be helpful in these cases to prevent wasted / low-impact effort. Having said that, even if we decide to avoid a fully-responsive design, at least we need to make sure our designs are not broken, cropped or distorted when the users view our website from their mobile or tablet devices.
Similar recommendations could be applied to cross-browser compatibility. It is best to do what works better for the nature of our product. If we can negotiate with our large B2B customers to support only Chrome, than cross-browser compatibility will be in the bottom of our priority list. This obviously wouldn’t apply to B2C web apps due to the variety of potential consumers. However, for our B2C, we could start by eliminating your support for older browsers in the beginning. If we were to do a market survey, most likely a tiny portion of our demographic would be using these old-fashioned browsers. Same applies to the mobile browsers. Most people keep their mobile browser versions up to date with automatic updates but I have seen a few instances where automatic updates were turned off on the consumer phone. A support case helped them to enable it back on and get away with no investment on that end.
Picking a CSS pattern like OOCSS, SMACSS, BEM is highly undervalued, especially in start-ups and in general by back-end focused developers. However I see good value in choosing the right CSS pattern and sticking with it through the journey. Standardisation of our CSS approach will pay off in the long run as it will decrease that tedious decision making process of picking css names and css code will be more predictable and maintainable. Do not spend too much time on this. Select the one that is suitable for you and move on.
3. Other Technical Concerns
Applying automated testing to keep our sanity.
Ignoring automated testing, especially for our core workflows could be pretty detrimental. We would not want to see main functionalities falling apart due to unprecedented errors 1 hour before an investment meeting (speaking from experience here). We should write automated tests and reduce our stress levels at release times. The more agile we get, the more frequent releases we will have. They are meant to be useful updates that customers should be looking forward to, not our worst nightmares.
Most engineers are on the consensus that well-written automated tests will outrun not having them in the long run. Start-up customers have more tolerance for mistakes, however with adequate testing coverage we will avoid late nights, extra frustration and extra stress for our own mental well-being. Remember it is not a sprint, it is a marathon.
I’m not a big fan of forcing standards like TDD especially since it demands a considerable shift in the mindset and some devs find it hard to work with what. This also comes down to the above discussion of not setting rigid rules that would slow down our development processes (and perhaps hiring in this case). However, we could apply TDD while fixing bugs and it is much easier than applying TDD on a bigger scale.
We should have a good coverage of end-to-end tests for the core workflows. There are powerful and popular tools like Selenium to do the heavy-lifting. We wouldn’t want to build our custom automation framework or anything similar at this point; the timing is just not right for this sort of big scale investments.
As “The Testing Pyramid” suggests, we could create integration tests for the major success and fail scenarios and introduce wider test coverage with unit tests to handle the edge-cases. Having said that, nowadays we can run our integration tests much faster with utilising new technologies like containers. (E.g. by resetting DB states conveniently between test transitions) If integration tests don’t cost that much time any more, we could slightly violate the pyramid and increase the number of our integration tests. This is not the topic of this conversation and there is no silver bullet for testing, so I will not discuss the pros and cons of this approach. Again, no hard rules on this.
Having our back-end exposed as a web API has tremendous benefit in preparing integration tests . We can utilise a third party product like Postman and write lightweight API test functions with the relevant data permutations and combinations for our endpoints. We could build multi purpose integration tests by including endpoint and functional testing to the same testing suites.
As far as the unit tests are concerned, my advise is to avoid testing thin layers like Controllers in an MVC or trivial code like null argument exceptions. There is also no need to duplicate mapper unit tests (operator overloading) even if they are exposed differently on an interface. Keeping our architecture layers separated (as discussed earlier) will help us to write unit tests easier without having to mock unnecessary dependencies in irrelevant layers. For example, if third party libraries interfere with our domain layer, we will waste plenty of time on mocking dependencies only to unit test simple business logic.
Always consider to add unit and integration tests for the bugs that we resolve. This is one of the areas where tests provide the most value.
When it comes to load and stress tests, they are of less importance but if we feel like implementing them, I’d prefer the former to the latter as we won’t have that many customers in our earlier days.
UI tests are one of the hardest to implement, so if we are not building a heavily-used front-end product like Canva, our end-to-end tests might do the job for these initial stages.
Performance tests are debatable and depends on the product once again. It would be ok to create them if they are necessary for our business cases.
Lastly, we should avoid writing automated tests for experimental code that we might throw away. We will end up with large chunks of redundant test code and we are not a Google that has the luxury to recycle plenty of code on a daily basis.
Avoiding to reinvent the wheel.
Simple advice but avoid to reinvent the wheel at all costs in these early days. Use whatever solid tool & framework there is out there in the market. Spend enough time to compare the pros and cons for each. I have seen various bad / poorly done custom implementations in startups including but not limited to message buses, schedulers and front-end tools. We will not only end up wasting time but also will implement sketchy and patchy replacements.
Having said that, be careful about mission critical components. If our application is all about reporting, we would not want to delegate it to a third party library. (Especially if they have crappy support)
DI could be very useful when working with these third parties. If we’d like to start with a really simple library to just get the job done but have a future plan to switch to another library, DI will give us the abstraction power to avoid tight coupling our code with the externals and cut down on the refactoring time when there is a need to switch.
Choosing the tech stack wisely.
I would suggest to stick with a good combination of what we already know and what could be a game-changer (if applicable) for our product.
Utilising our existing knowledge, languages, tools will dramatically speed up the product development process and speed is the number one essential thing when we are an early stage start-up.
We shouldn’t get lured by a fancy technology we have always wanted to work on. This is not our playground.
Documenting the decisions.
We should draw the current and desired future architecture diagrams and provide easy access to everyone. This will avoid potential confusions along the journey.
Do not disregard the importance of briefly documenting our architectural decisions. This will not only help others but also help us when we forget about the details of our own decisions. And it happens more often than one would think. This practise will be a beneficial habit for us later on and lay the foundation for our future wiki.
Lightweight documentation example:
Q: Why have we decided that X (3rd party tool) will be more beneficial for our application compared to Y even if Y looks like a better architectural fit?
A: There haven’t been any release updates on Y over the last 3 years, we may not get enough support for the tool.
To sum up, we should leave our egos behind, avoid perfection, move fast and build everything lightweight with the future in mind.
Best of luck...
(and don’t forget to eat & sleep)