Insiders’ Email Product Monorepo Journey
This document outlines the advantages of a Golang mono repository and how to best implement it. As each organization and its developers are unique, the approach and benefits that come with a mono repository can vary. For example, the Insider Email product team has been utilizing mono repository logic for over three years and it has enabled us to work more efficiently.
In this article, I will explain our approach to using a mono repository and the various advantages it has provided us with. For instance, the use of a mono repository has allowed us to more easily manage code dependencies, streamline development processes, and better manage code versions. Additionally, a mono repository provides us with improved visibility into the codebase, allowing us to easily pinpoint any issues that may arise. Ultimately, the use of a mono repository has allowed our team to develop better and more reliable products, something that we highly value.
Let me tell you a little story about it how we develop our product at first.
We were working on a different product team at Insider called Architect. With Architect, customers have the capability to build customer journeys according to their own needs. For example, if customer X adds product Y to their cart but does not complete the purchase within three days, they could be sent a reminder via SMS, an app notification, or a web-based push notification to remind them to buy the item. In addition, we (as Insider) didn’t have an Email product at the time, and we realized that having one would be beneficial for us and our customers alike. We conducted further research in order to understand the need for an Email product, and gathered insights from other companies in the industry. After careful consideration, we decided that an Email product was a necessary addition to our product portfolio, and we went on to create an innovative Email product that could provide our customers with an even better customer experience.
The Email product was started in Insider as a PHP Laravel project 4 years ago, with the intention of creating another channel for the Architect product. Over time, we realized that Email had the potential to become its own product, while still keeping the Architect Emails as a channel. When we launched our new product, PHP Laravel wasn’t a good fit, so we switched to Golang immediately. We initially chose PHP because it enabled us to quickly bring our product to market, it was about team knowledge and technical abilities. After we switched to Golang, we decided to use NSQ as a message broker with a publish/subscribe architecture.
We created two new repositories, email-queue-producer,
and email-queue-consumer
, both written in Golang. It's quite simple to manage; we only have two projects.
Let me share the project structure with you.
Email Queue Consumer
app
├── adapters
├── cache
├── config
├── consumers
├── email
├── helpers
├── mock
├── models
├── params
├── services
├── template
└── utils
Each folder contains multiple .go
files and their associated test files.
Email Queue Producer
app/
├── api
│ ├── controllers
│ ├── middleware
│ ├── responses
│ └── validator
├── category
├── channels
├── config
├── coupon
├── email
├── emailtest
├── enums
├── events
├── experiment
├── formatter
.
.
.
├── scheduler
├── stats
├── storage
├── tasks
├── template
├── tools
├── transactional
├── types
├── ucd
├── unsubscribe
├── utils
└── warmup
Each folder contains multiple .go
files and their associated test files.
I know it doesn’t seem positive to you, or to us, in the aftermath of this disaster; it’s a difficult situation for everyone involved and it’s not easy to find a way out. We can only hope that with some effort and dedication, we can make the best of this and come out of it with a better outcome. Despite the difficulty of the situation, we must remain optimistic and work together to find a solution. With determination and perseverance, we can make this mess a distant memory.
After a while, we continued to add features to the Email product. We included transactional emails to enable users to receive and respond to system notifications, test emails to check that the email delivery was working effectively, and a variety of campaign types including send time optimization to ensure the emails are sent at the best possible time, A/B testing to compare two or more versions of an email, recurring emails to send automatic messages on a set schedule, and RSS emails to provide subscribers with the latest updates from a website. All of these features were designed to make the Email product more powerful and user-friendly.
The features that we continue to add to the product are essential for our success in the market, and they provide a great benefit to our customers; however, managing all these features with this code base was proving to be an increasingly difficult task. This was due to the fact that the code base was outdated and unable to accommodate the newer features, resulting in a lot of extra work and effort being put into making sure the features were properly implemented. We needed to find a way to upgrade the code base so that it could better handle the newer features, without compromising on the stability and security of the existing code. We eventually identified a suitable solution that allowed us to manage all of the features effectively, without sacrificing the quality of the product.
As an Insider Engineering Team, our commitment to excellence is unwavering; we are constantly challenging ourselves and striving to become better engineers and people than we were the day before. We are dedicated to creating innovative solutions that push the boundaries of what is possible and use technology to make an impact on the world. We take pride in our work and strive to exceed expectations every day. Our mission is to create something that will last and make the world a better place in the process.
In 2020, we realized that we couldn’t continue using our current code structure to improve our product. Therefore, we decided to switch to a mono repo codebase. Initially, it was a relatively simple process and looked something like this:
├── cmd
│ ├── event-consumer
│ ├── event-publisher
│ │ ├── handlers
│ │ └── internal
│ │ └── event
│ ├── filter
│ ├── publisher
│ └── scheduler
└── internal
├── config
├── event
├── mail
├── platform
│ ├── cache
│ ├── database
│ ├── messenger
│ ├── retry
│ └── storage
└── user
At first glance, the code base appeared to be easy to use. However, as we added more features over time, it became difficult to use once again. To be honest, this design only had two minor issues.
Issue 1: Why do we have two different internal folders?
We made a decision to use a package called “internal” within every cmd/{service-name}
directory. However, this was a bad decision because it caused confusion for new developers joining our team. Specifically, the new developer we onboarded for the project struggled with the idea of having an internal package in two different places. One of these places was under the root project folder (which is fine), but the other was under cmd/{service-name}
. This approach made it difficult for them to determine where to place specific packages, leading to frequent questions about whether a package should be under root/internal
or cmd/{service-name}/internal
.
Issue 2: What is the purpose of the “platform” folder under the “internal” folder?
We have decided to add packages that can function independently, without dependencies on other packages. These are packages that can be easily copied and pasted into other projects.
I’m calling you from the year 2023...
In the end, we come up with another solution, currently, we are mostly following this documentation for most of our projects but of course, we are not doing everything like in the documentation. Here is the documentation;
https://github.com/golang-standards/project-layout
I would not recommend this project layout for very small Golang applications. Even the creator of the layout does not recommend it.
What we are doing differently?
- We do not have a folder called
api
under the root folder. Instead, we are adding the API layer undercmd/{service-name}
.
That’s all. When you put your API layer under the root folder, it can be difficult to follow the code in the navigator and find what you are looking for.
This is our final look at one of our projects;
├── cmd
│ ├── event-publisher
│ │ └── api
│ │ ├── handlers
│ │ ├── mid
│ │ └── rest
│ │ └── main.go
.
.
.
.
├── codebuild
├── internal
│ ├── config
│ ├── consumer
│ ├── events
├── pkg
│ ├── cache
│ ├── log
│ ├── request
│ ├── retry
│ ├── sns
│ ├── sqs
│ └── unique
How to Dockerize these types of applications?
Below you can see the Dockerfile that you can use for mono repo applications for security reasons of course I did some small changes to the Dockerfile that we use for our projects but the example Dockerfile will do the job for mono repo applications
FROM golang:1.18 as builder
LABEL MAINTAINER="@dogukanayd <thisisyou@useinsider.com>"
ENV CGO_ENABLED 0
ENV SERVICE_PATH /go/src/mono-repo-example
ARG PACKAGE_NAME
ARG VCS_REF
# Directory to build the applocation
RUN mkdir -p ${SERVICE_PATH}
# Copy source files
WORKDIR ${SERVICE_PATH}
COPY go.mod go.mod
COPY go.sum go.sum
COPY cmd cmd
COPY internal internal
COPY pkg pkg
RUN go mod vendor
# Build the binary
WORKDIR ${SERVICE_PATH}/cmd/${PACKAGE_NAME}
RUN go build -ldflags "-X main.build=${VCS_REF}"
# Running the build
FROM alpine:3.7
ENV SERVICE_PATH /go/src/mono-repo-example
ARG BUILD_DATE
ARG VCS_REF
ARG PACKAGE_NAME
COPY --from=builder ${SERVICE_PATH}/cmd/${PACKAGE_NAME}/${PACKAGE_NAME} /app/main
COPY .env /app/.env
WORKDIR /app
CMD ["/app/main"]
If you are interested in real-life examples, please look over How to schedule scaling (without writing a single line of code) on Amazon DynamoDB provisioned tables post by Ersoy Pembe, Senior Staff Engineer at Insider.