This is just an example of a monorepo design where multiple languages are kept under same repository. Each language has its own folder. You could also split monorepo into language basis rather than keeping all the languages in same repository. It is all up to you and your needs.


Here we are going to work with Go language to give you an idea. There will be three services and each will be able to talk to each other. When it comes to testing, we will build a Docker image that contains a binary where you pass the service name to start it. We will then deploy this image to Kubernetes production cluster to run all three services. All services have endpoints for us to call from outside. Also, there will be some endpoints for service-to-service communication.


What you need to keep in mind is that, given this is a dummy/unfinished example, there will be some duplications and less valued parts. The main point is to give you an idea!


Structure


Description


├── go    # where go stuff lives
├── doc # where documentation lives (meant to cover all languages)
├── infra # where infrastructure stuff lives (meant to cover all languages)
├── js # where javascript stuff lives
└── php # where php stuff lives

Example


├── .gitignore
├── go
│   ├── cmd
│   │   └── monorepo
│   │   └── main.go
│   ├── go.mod
│   ├── pkg
│   │   └── client
│   │   └── http.go
│   └── svc
│   ├── account
│   │   └── main.go
│   ├── exam
│   │   └── main.go
│   └── student
│   └── main.go
├── infra
│   ├── dev
│   │   ├── Makefile
│   │   └── docker
│   │   └── docker-compose.yaml
│   ├── prod
│   │   ├── Makefile
│   │   ├── docker
│   │   │   └── go
│   │   │   └── Dockerfile
│   │   └── k8s
│   │   └── go
│   │   ├── account.yaml
│   │   ├── exam.yaml
│   │   └── student.yaml
│   └── qa
│   ├── docker
│   └── k8s
├── js
│   └── ... put your stuff here
└── php
└── ... put your stuff here

Files


.gitignore


go/bin

dev/docker-compose.yaml


version: "3.4"

services:

monorepo-mysql:
container_name: "monorepo-mysql"
image: "mysql:5.7.24"
command:
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "root"

dev/Makefile


.PHONY: help
help: ## Display available commands.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

# DOCKER -----------------------------------------------------------------------

.PHONY: dev-docker-up
dev-docker-up: ## Bring dev environment up in attached mode.
docker-compose -f docker/docker-compose.yaml up --build

.PHONY: dev-docker-down
dev-docker-down: ## Stop and clear dev environment.
docker-compose -f docker/docker-compose.yaml down
docker system prune --volumes --force

.PHONY: dev-docker-config
dev-docker-config: ## Echo dev environment config.
docker-compose -f docker/docker-compose.yaml config

prod/Dockerfile


FROM golang:1.17.5-alpine3.15 as build
WORKDIR /source
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o monorepo cmd/monorepo/main.go

FROM alpine:3.15
COPY --from=build /source/monorepo /monorepo
EXPOSE 8000
ENTRYPOINT ["./monorepo", "--svc"]

prod/Makefile


.PHONY: help
help: ## Display available commands.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

# CI/CD ------------------------------------------------------------------------

.PHONY: docker-push-go
docker-push-go: ## Build, tag and push go image to registry then clean up.
DOCKER_BUILDKIT=0 docker build -t you/monorepo-go:latest -f docker/go/Dockerfile ../../go
docker push you/monorepo-go:latest
docker rmi you/monorepo-go:latest
docker system prune --volumes --force

.PHONY: k8s-deploy-go
k8s-deploy-go: ## Deploy go applications.
kubectl apply -f k8s/go/account.yaml
kubectl apply -f k8s/go/exam.yaml
kubectl apply -f k8s/go/student.yaml

prod/k8s file


I am not going to duplicate all three yaml files but you can. If you do, just replace account word with student and exam.


apiVersion: v1
kind: Service
metadata:
name: svc-account
namespace: prod
spec:
type: ClusterIP
selector:
app: account
ports:
- port: 8000
targetPort: 8000

---

apiVersion: apps/v1
kind: Deployment
metadata:
name: dep-account
namespace: prod
labels:
app: account
spec:
replicas: 1
selector:
matchLabels:
app: account
template:
metadata:
labels:
app: account
spec:
containers:
- name: go
image: you/monorepo-go:latest
args:
- account
ports:
- containerPort: 8000

go/cmd/main.go


package main

import (
"flag"
"fmt"
"log"
"os"

"monorepo/svc/account"
"monorepo/svc/exam"
"monorepo/svc/student"
)

const usage = `Description: Service to run
Usage: %s [options]
Options:
`

func main() {
var svc string

flag.StringVar(&svc, "svc", svc, "Service name.")
flag.Usage = func() {
_, _ = fmt.Fprintf(flag.CommandLine.Output(), usage, os.Args[0])
flag.PrintDefaults()
}
flag.Parse()

switch svc {
case "account":
account.Start()
case "exam":
exam.Start()
case "student":
student.Start()
}

log.Println("Unknown service")
}

go/pkg/http.go


package client

import (
"net/http"
)

func HTTPRequest(uri string) error {
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return err
}

if _, err := http.DefaultTransport.RoundTrip(req); err != nil {
return err
}

return nil
}

go/account/main.go


package account

import (
"log"
"net/http"

"monorepo/pkg/client"
)

func Start() {
rtr := http.DefaultServeMux
rtr.HandleFunc("/pay-debt", func(http.ResponseWriter, *http.Request) {
log.Println("/pay-debt")

if err := client.HTTPRequest("http://svc-exam:8000/unlock-results"); err != nil {
log.Println(err)
}
})

srv := &http.Server{Handler: rtr, Addr: "0.0.0.0:8000"}
if err := srv.ListenAndServe(); err != nil {
log.Fatalln(err)
}
}

go/exam/main.go


package exam

import (
"log"
"net/http"

"monorepo/pkg/client"
)

func Start() {
rtr := http.DefaultServeMux
rtr.HandleFunc("/unlock-results", func(http.ResponseWriter, *http.Request) {
log.Println("/unlock-results")
})
rtr.HandleFunc("/publish-results", func(http.ResponseWriter, *http.Request) {
log.Println("/publish-results")

if err := client.HTTPRequest("http://svc-student:8000/publish-results"); err != nil {
log.Println(err)
}
})

srv := &http.Server{Handler: rtr, Addr: "0.0.0.0:8000"}
if err := srv.ListenAndServe(); err != nil {
log.Fatalln(err)
}
}

go/student/main.go


package student

import (
"log"
"net/http"

"monorepo/pkg/client"
)

func Start() {
rtr := http.DefaultServeMux
rtr.HandleFunc("/pay-debt", func(http.ResponseWriter, *http.Request) {
log.Println("/pay-debt")

if err := client.HTTPRequest("http://svc-account:8000/pay-debt"); err != nil {
log.Println(err)
}
})
rtr.HandleFunc("/publish-results", func(http.ResponseWriter, *http.Request) {
log.Println("/publish-results")
})

srv := &http.Server{Handler: rtr, Addr: "0.0.0.0:8000"}
if err := srv.ListenAndServe(); err != nil {
log.Fatalln(err)
}
}

go.mod


module monorepo

go 1.17

Docker push


Run monorepo/infra/prod$ make docker-push-go command to build and push docker image for go services. If you want to manually test the service in local environment you can do the following. You can then use http://0.0.0.0:8888 to test student service endpoints.


monorepo$ docker build -t you/monorepo-go:latest -f infra/prod/docker/go/Dockerfile go/
monorepo$ docker run --name monorepo-student -d -p 8888:8000 you/monorepo-go:latest student

$ docker ps
IMAGE COMMAND PORTS NAMES
you/monorepo-go:latest "./monorepo --svc student" 0.0.0.0:8888->8000/tcp, :::8888->8000/tcp monorepo-student

Deploy to Kubernetes


Run $ kubectl create namespace prod command to create the namespace first and then run monorepo/infra/prod$ make k8s-deploy-go command to deploy go services. You should see resources as shown below.


$ kubectl -n prod get all
NAME READY STATUS RESTARTS AGE
pod/dep-account-6db8bb9c68-v5dr6 1/1 Running 0 18s
pod/dep-exam-54b78448d-nd6js 1/1 Running 0 17s
pod/dep-student-6fc98cc9dd-qpxwc 1/1 Running 0 17s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/svc-account ClusterIP 10.108.90.79 8000/TCP 18s
service/svc-exam ClusterIP 10.108.226.180 8000/TCP 18s
service/svc-student ClusterIP 10.109.116.75 8000/TCP 17s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/dep-account 1/1 1 1 18s
deployment.apps/dep-exam 1/1 1 1 17s
deployment.apps/dep-student 1/1 1 1 17s

NAME DESIRED CURRENT READY AGE
replicaset.apps/dep-account-6db8bb9c68 1 1 1 18s
replicaset.apps/dep-exam-54b78448d 1 1 1 17s
replicaset.apps/dep-student-6fc98cc9dd 1 1 1 17s

Test


First of all make your services accessible from your local environment with command below.


$ kubectl -n prod port-forward service/svc-student 8001:8000
Forwarding from 127.0.0.1:8001 -> 8000
Forwarding from [::1]:8001 -> 8000

$ kubectl -n prod port-forward service/svc-exam 8002:8000
Forwarding from 127.0.0.1:8002 -> 8000
Forwarding from [::1]:8002 -> 8000

$ kubectl -n prod port-forward service/svc-account 8003:8000
Forwarding from 127.0.0.1:8003 -> 8000
Forwarding from [::1]:8003 -> 8000

You can now use endpoints below and watch pod logs for confirmation.


http://127.0.0.1:8001/pay-debt (external for you to consume)
http://127.0.0.1:8001/publish-results (internal for exam service to consume)

# Exam
http://127.0.0.1:8002/publish-results (external for you to consume)
http://127.0.0.1:8002/unlock-results (internal for account service to consume)

# Account
http://127.0.0.1:8003/pay-debt (internal for student service to consume)