Software Architecture of Increaser App
In this story, we are going to look at the architecture of the Increaser app and lessons learned on the road of development. From the programmer's perspective, this program is a Pomodoro timer on steroids with projects, tasks, and statistics. With Increaser we want to help people master their time, so they will find time for things that matter. If you interested in mindset and StoryBrand of Increaser check these stories. Let’s dive into how it works!
In this story, we will cover three parts of the app and how they work together.
- Front-end(React, Redux, Redux-Saga, Styled-Components)
- Back-end(NodeJS, Express, DynamoDB, S3)
- DevOps(AWS, Terraform)
Technology Stack and First Lesson
Since we had long experience working with React, Redux and Saga we decided to choose these technologies for development. For styling, we usually used SCSS and were accustomed to it, yet we wanted to try a relatively new thing in the field CSS-in-JS, particularly Radium library. But after some time we came to a decision — better start using styled-components before it is not too late. And this decision was made because at the same time we worked on projects with styled-components. After having experience working with this library, we start seeing that it is much convenient than Radium. Some of the benefits are nice VS code extension, more declarative JSX with tags that talk by themselves and more flexibility. Now we have a mix of two libraries in the project and start removing Radium. The lesson that comes from this is — you need to research what technologies you are going to use before starting a new project since it is better to work with convenient tools. We had experience working with different approaches for styling in React, and now styled-components will be the way to go when we are starting the new project.
Create-React-App and Redux/Saga
We always use create-react-app to bootstrap a new project. if you are interested in what I am doing after running create-react-app — check this story. The main lesson we learned while developing Increaser is that it is better to export not reducer, but a function that returns reducer, and the same thing with the default state. And this is because we want to have a brand-new state after a user logs out from the app. Now, the root reducer in our app looks like this:
Before starting developing Increaser, we decided not to use URLs in it and not to the user react-router library. But how we manage navigation without react-router?
First, we have a file with all the pages listed. And it looks like this:
To decide which page to render we have root component that looks like this:
And the function we use to connect to the state is a little wrapper that simplifies imports.
We didn’t need URLs, but after some time we decided that we need to work with browser history API to make browser back and forward buttons work. And it is especially important if you want your app used on mobile phones. To implement this functionality, we upgraded our navigation reducer to update history state after we change the page component.
Each day a user makes some Pomodoro sets, and he can have thousands of sets after some time. The reason why statistics is so fast is that we have ready for fast rendering data on the back-end and we are saving statistics in the browser, so when the user goes to the statisticcs page we only need to request part of data. To show statistics, we using a recharts library. And recently we start implementing our own charts library specifically for Increaser since we weren’t able to achieve the result we want with existing ones.
Recently we decided to create the app that will have only Pomodoro timer and nothing else. And we already build core components for it. So we moved them to NPM modules. If you interested in them they listed below.
As with the front-end, we decided to use technologies we worked with recently — NodeJS(Express) + DynamoDB. I already wrote two stories about testing and migrations with DynamoDB, therefore, we only look at how data organized in the database.
When we decide to use a NoSQL database we need to understand what queries we will make in the future. And then plan how to organize data and relations between objects.
Table With Users
As a key for user-item, we are using generated on the first authorization ID. In the item we keep essential information about user received after authorization with Google, projects, tasks and Pomodoro sets.
We keep finished projects/tasks and projects/tasks in different arrays so that it will be easier to take what we need from the database.
As soon as the user finish Pomodoro set we update the task item with new data. And then we use this information to determine how much time a user spends on this task, what is an average set and the percentage of stopped ones.
We can’t save all Pomodoro sets in one item since DynamoDB has an item size limit. So when the number of sets becomes more than the specific number we are moving them to S3 bucket. We are planning to provide some useful insights for the user about how to improve his productivity based on this data.
Table With Statistics
We need to provide information about how the user spends his time for specific periods — days, weeks and months. To do this, we have a statistics table. Key in this table has two parts — user id and name of the period. As soon as the user finishes his set, we will update the last item in days, weeks, month table with new data.
As with front-end and back-end, we decided to use what we already know — AWS and Terraform.
The project contains five directories. One with CI docker images and script for pushing those images. We have stateless modules that may be reused between different environments and directory with global resources. In dev and prod directories we have resources created by using global and modules.
Resources for front-end and back-end
For front-end hosting, we are using AWS CloudFront. Certificates provided by AWS Certificate Manager. To run back-end, we are using AWS Elastic Beanstalk. While working with infrastructure for Increaser, we made two mistakes. The first one was not to use Terraform in the first place and doing everything by hand. And when we moved everything on Terraforms is using local state. Recently we moved some state to S3 since we import route/certificates variables for pomodoro.increaser.org. If you interested in how to use terraforms to host SPA, take a look at this story.
In the previous project, we used bitbucket-pipelines for CI/CD. But this time we decided to use CodePipeline since it is easier to provide env variables to CI container while using terraforms. With CodePipeline we can pass all env vars in the pipeline module, and that it. When we use bitbucket-pipelines, we need to make sort of hacks, like running script after resource state update that will set env variables via Bitbucket API.
In this story, we had a look at the architecture of Increaser, if you have some suggestions what can be improved, please leave a comment:)
Reach the next level of focus and productivity with increaser.org.