From a Side Project to a Startup — the perspective of a backend developer

TH Huang
Cooby HQ
Published in
9 min readAug 11, 2021

Turning a side project into a profitable business is always a sweet dream for software developers. Before the masterpiece is commercial, it usually must be in production. However, there is no such universal standard or framework which could tell you whether the application is production-ready or not. It is not a single switch but an iterative process that turns your project toward a robust system. As long as you and your team are comfortable about its reliability, you are good to say your application is production-ready!

The vision of Cooby is to provide software applications and solutions to help sales and companies manage their customer relationships with superior quality and high efficiency. We have developed several products to support and achieve our mission. Indeed, underlies all of our products is a single backend system. It supports every API request from all mobile and web applications. Let’s fast forward to the beginning of our journey and walk through the decisions and refinements we have made to our system!

Standard Client/Server Setup

In the context of software system design, everything is a trade-off. There isn’t the best design for all kinds of systems and use cases. Instead, we have to make the most plausible decision depends on our situation at a particular time. It is crucial to define a clear scope and set reasonable goals for the system first.

Our products include mobile applications and web applications at the same time. We separated the server and the client from the beginning of our architecture design since we expected to have multiple traffic sources to the server. In addition, servers with a single responsibility are more precise and cleaner in our sense. Therefore, the scope of the server development could be minor down to a pure backend API service.

Reliability and Security

Reliability is probably the single most important thing that attracts censorious attention from your existing users. We have the responsibility to provide a system that is reliable enough. But yes, I said “reliable enough.” At the infant stage of the startup, developing prototypes and seeking the PMF is much more critical to us. On the other hand, early users might not leave due to bugs from corner cases, but they will definitely abandon the product if it doesn’t solve their pain point. As a result, end-to-end tests that cover all of the happy cases might be enough for now.

Security is also part of the reliability assessment. We regarded it to be one of the essential facades of reliability. Moreover, users pay more attention to it nowadays. We designed and developed our software with privacy and security at heart. Fortunately, these security best practices are conventional in the industry. We could also find their implementations in tools and libraries for every mature programming language or web application framework.

Scalability and Performance

Scalability and performance is often the first thing we will consider as a backend engineer. We usually are trained to design systems that could cope with large concurrency and intensive loads. However, in the context of an early startup, even the number of the DAU of the product could be less than the QPS we could have in a mature software application. It is nice to keep it in mind, but we should never over-engineer for scalability at this stage.

Maintainability, Agility, and Extensibility

When it comes to maintainability, we often think of working with legacy codes, fixing bugs, and keeping the old system running for a long time. We might not consider it to be essential at the beginning of the product development. Actually, the CEO, the product lead, and even our users don’t care. But we, developers, should! Keeping a high standard in maintainability will make our life much easier in the foreseeable future.

Maintainability means a lot more than dealing with existing codes. It implies that the architectural decision we make and the codes we write today affect every day afterward. Extensibility, agility, and readability matter the most among all other aspects for a startup software system.

Modularity

While minimizing the short-term development cycle, we also focused on providing a clear boundary and an individual responsibility to each system module. Adopting architectural patterns with reasonable abstractions is a great practice that leads to a more flexible and reusable system. Finally, we always placed a high priority on maintaining good modularity. A small break in modularity to speed up development might seem harmless. However, it would be a nightmare to resolve circular dependencies at midnight when dealing with an outage.

Building Blocks

Since agility and extensibility are the top priorities of our architectural characteristics, we could start evaluating and defining the building block for the system. As a startup, “time” is one of the most precious assets for us. We strived to minimize the design-to-deliver time to rule out false hypotheses to the market as fast as possible. Specifically, we should optimize the development cycle for building multiple revisions of MVPs. Thus, we expected that various versions of products would exist in the same codebase at the same time.

We should devote all of our time to the core business logic, which differentiates us from others. Thus, utilizing third-party services and tools to avoid building existing wheels is our first choice. In addition, deploying in platform-as-a-service also reduces our effort in dealing with the infrastructure.

The product business domain influences the system architecture decision. Nevertheless, the choice of the building blocks also highly depends on developers’ background knowledge and experience. Since we are not building technology-intensive services, deciding which external service to use in each building block is not our focus. We tend to choose services that we are already familiar with to speed up the integration process.

In the context of Cooby, our developers were competent in developing full-fledged backend services and working with containers but relatively new to serverless concepts. We decided to build our server with a flexible framework and deployed it on a fully managed cloud platform.

We developed our backend application with Flask and deployed it on Heroku. With the integration of multiple external services, we built a complete backend system within a few weeks.

Infrastructure

One of our goals is to minimize the work for orchestrating infrastructure. At the same time, we would also like the workflow to be friendly for every developer. Thus, platform-as-a-service is the most suitable solution for us despite its higher price than building from scratch with IaaS solutions.

  • Heroku is the platform where we deployed our backend application. We could effortlessly deploy and manage our application without spending tons of time configuring the infrastructure. The instance can be built from a docker image or managed by Git easily. Additionally, we could integrate multiple useful add-ons with only a few clicks.
  • GitHub Actions is a mere yet sufficient solution for building our minimum CI pipeline. It is included in GitHub standard plan; thus, there is no additional integration needed. Currently, we have only a single workflow for running all the test cases and calculating the coverage.
  • AWS Lambda, a serverless computing service, and AWS SQS, a message queue service, are integrated into our system for specific use cases. They are the basic building blocks for Cooby Insights and our messaging app integrations.

Application

Our developers are from diverse backgrounds with expertise in different programming languages and frameworks. We have no subjective preferences on which technology stack to use. The goal is to minimize the development time and make our delivery cycle swift and smooth.

  • Python is the primary programming language of our backend codebase. We chose Python due to its large community and reputation for its efficiency in development.
  • Flask is our web framework. We designed to confine the Flask application in the presentation layer. Precisely, it usually handles only web-related tasks, such as routing and parsing HTTP requests. Certainly, the HTTP protocol is the default communication protocol between the client and the server.
  • Gunicorn is the WSGI HTTP server implementation for running our Flask application. The Eventlet library is for achieving network concurrency. Since we are not going to fine-tune the performance of the underlying WSGI configuration at this stage, it is standard to use Gunicorn and Eventlet with Flask.

Data Storages

  • PostgreSQL is the primary database. There isn’t a dominating reason for choosing a specific data model in our situation. Thus, using a relational database is the definitive decision since almost every engineer is familiar with SQL. Since PostgreSQL is one of the add-ons supported in Heroku, we chose it over MySQL or other relational databases.
  • Redis is the in-memory data store. Instead of caching, it is excellent for persisting entity status across multiple server instances in a limited period.
  • AWS DynamoDB and AWS S3 are persistence storages for saving self-contained information or static data. E.g., we store chat metadata in DynamoDB and image contents in S3.

Tools

Perhaps logging and monitoring are the most crucial aspects for maintaining the reliability of a production application. They give developers as well as the entire team more confidence to release the feature more often. Consequently, we can publicize our MVPs to the market much faster.

  • Datadog is for defining metrics and monitoring the state of the application and the infrastructure. The tracking and alerting utilities are handy for detecting abnormal events. Furthermore, we could also extract meaningful business insights from the statistical user behavior visualizations.
  • ELK Stack/Elastic Stack is for logging. It provides intuitive data query capability and data visualization modules. Logging is so critical as it often is the only source for debugging for users. With this powerful instrument as our logging service, we could efficiently pinpoint the root cause of every bug and fix them in no time.
  • Codecov is for tracking the code coverage of the codebase. Executing and achieving the test coverage guideline is as important as developing it. With the guidance from Codecov, we could know whether our coverage agreement is on track.

Third-party Integrations

There is no reason for us to develop tools and services if mature solutions already exist. Utilizing third-party integrations is like standing on the shoulders of giants. We could focus on the development of our core technology without having to worry about building auxiliary modules.

  • Firebase provides various valuable necessities for constructing a complete backend system. Our system highly depends on Firebase Remote Config to dynamically control the application behaviors and Firebase Cloud Messaging for sending push notifications to all platforms.
  • MailChimp is the platform for email marketing as well as sending transactional emails to specific users. It helps when implementing features such as resetting passwords and notifications from asynchronous services.
  • Stripe is the online payment solution for our products. Although a payment system is imperative for every business, it usually is too complicated and time-consuming to implement from scratch. Stripe handles the integrations to different financial services and provides a single API interface. Moreover, it covers the entire tedious payment process from subscription management and invoicing to financial reports generation.

The Backend Server Architecture

After delegating subsidiary components to external services, we could exclusively commit to designing and developing our backend application. As a high-level overview, we referenced the concept from the domain-driven design but allowed a more flexible implementation for the domain logic. Besides, we wrapped external services as reusable components for having better control of their usage.

Each Application represents a single implementation of our product. The Base is for general-purpose code, and Services are wrappers for external dependencies.

Interactors are the heart of our backend system. It is the layer that performs the domain actions, responsible for handling the business logic. Entity, the domain object, is the core of the interactor. The restriction is that it shall not have any dependencies on the outer layers.

Flask app is the entrance of the application. Nonetheless, the presentation layer confines its responsibility. It handles HTTP requests as well as provides some helpful command-line interfaces. Generally, it serves as the endpoint to our interactors.

Repositories implement the interfaces defined in interactors. They act as the persistent layer for interactor entities. However, as an entity, it need not and shall not know where and how it is stored.

Services encapsulate external dependencies. We reshaped the interfaces from the third-party libraries and provided consistent internal interfaces for every application. Furthermore, We included error handling, usage logging, and monitoring in the wrapper for achieving better reliability.

Base is for general-purpose code and project-specific functionality. We decoupled these modules out as individual subproblems. Besides, application-specific configs and utilities are moved into the scope of each application.

Epilogue

With our initial architectural setup, we have constructed an efficient backend development process. We could support our tight application release schedule for every product on every platform. Usually, there are a minor version update each week and a major feature release every three weeks.

Architectural design is an endlessly iterative process. Fortunately, we don’t need perfect architecture to construct a backend system. A practical architecture as the starting point is already helpful. We could always refactor and migrate the system if any architectural characteristics become essential in the future. Having a continuous journey is also the beauty of backend engineering.

Below are some references we have considered during the design of our architecture. We will cover a concrete example of our backend system in a future post.

In the comment below, let us know your thoughts on the ideal setups and trade-offs to build a production-ready system. Thanks for reading!

References

  1. Netflix Technology Blog, “Ready for changes with Hexagonal Architecture.”
  2. Martin Kleppmann, “Designing Data-Intensive Applications.”
  3. Harry J.W. Percival & Bob Gregory, “Architecture Patterns with Python.”
  4. Mark Richards & Neal Ford, “Fundamentals of Software Architecture.”
  5. Flask Official Documentation.
  6. Github: cookiecutter-flask/cookiecutter-flask.

--

--