This repo contains the implementation of the backend task shared via email. The following tech stack is used:
- Gin framework in Golang
- Docker
docker-compose
- Postgres for DB
- JSON for configs that can be exported to Vault (or similar tools)
Bash
for scripting along withMake
I have not worked with NodeJS in depth, hence the choice of going with Golang.
Folder Structure using `tree`
├── cmd
│ └── main.go
├── config
│ └── config.json
├── docker-compose.yaml
├── dockerfiles
│ └── Dockerfile
├── go.mod
├── go.sum
├── Makefile
├── makefiles
│ ├── banner.sh
│ ├── build.mk
│ ├── help.mk
│ └── local.mk
├── README.md
├── scripts
│ └── container_bite_build.sh
└── src
├── app
│ ├── bite.go
│ ├── handlers
│ │ └── identify_handler.go
│ └── middleware
│ └── identify_middleware.go
└── pkg
├── database
│ ├── contact.go
│ └── database.go
├── models
│ ├── constants.go
│ ├── contact.go
│ └── identify.go
└── utils
├── config.go
├── logger.go
├── string.go
└── timezone.go
cmd
-> Contains the main.go
file which would be used to generate the binary
config
-> Contains the config for this project
dockerfiles
-> Self explanatory
makefiles
-> Using Make in this project to make it easier to use and replicate therefore some makefiles
scripts
-> Any random scripting that I have done for this project
src
-> Main application folder. Contains the app
folder which contains our service code & pkg
folder for peripheral services like db, logging, utils etc.
Before starting the app, it is important to setup the DB. Run the following command to start just the DB for some pre-liminary operations.
make setup.db
Re-run the command if you get any error. If the error persists, it might be something I have missed. Please reach out.
This command would remove existing data structures (if any) related to previous table. It would drop existing table and therefore all data as well. Now you would have 2 options:
- Seed some test data (from problem statement doc itself) to the DB
- Start testing with random values
Option 1 works for testing out scenarios given in the problem statement doc. To continue with that run the following command:
make seed.db
This would add data to the contact table created before using setup.db command. The table should look as follows:
id|phone_number|email |linked_id|link_precedence|created_at |updated_at |deleted_at|
--+------------+------------------------+---------+---------------+-----------------------+-----------------------+----------+
1|123456 |lorraine@hillvalley.edu | |primary |2023-04-01 00:00:00.374|2023-04-01 00:00:00.374| |
2|123456 |mcfly@hillvalley.edu | 1|secondary |2023-04-20 05:30:00.110|2023-04-20 05:30:00.110| |
3|919191 |george@hillvalley.edu | |primary |2023-04-11 00:00:00.374|2023-04-11 00:00:00.374| |
4|717171 |biffsucks@hillvalley.edu| |primary |2023-04-21 05:30:00.110|2023-04-21 05:30:00.110| |
cURL requests for testing out scenarios shared in the doc are given in upcoming sections.
To validate phone numbers, I have implemented a middleware called identify_middleware.go
which validates given phone number against a regex. Since option 1 seeds data that would not pass through the validation, we need to make some changes to the file before we start testing. Open the mentioned file and follow the commands given as NOTE
and trust the IDE to resolve errors. If that does not work, don't worry. You can proceed without phone number validation since it would work for both option 1 & 2.
Copy the following config (if different) to config/config.json
{
"DefaultTimezone": "Asia/Kolkata",
"Port": 8080,
"Mode": "debug",
"Databases": [
{
"Host": "db",
"Port": 5432,
"User": "dbuser",
"Password": "BitespeedTask!",
"Name": "bitespeed",
"IdleConnections": 0,
"OpenConnections": 50,
"Type": "write",
"SamplingRateInSeconds": 10
},
{
"Host": "db",
"Port": 5432,
"User": "dbuser",
"Password": "BitespeedTask!",
"Name": "bitespeed",
"IdleConnections": 0,
"OpenConnections": 50,
"Type": "read",
"SamplingRateInSeconds": 10
}
]
}
Run the following command
make run.all
This would internally call docker compose
and run both the app and the db. Keep in mind, this command would not detach the containers hence you would be able to see both application and DB logs. Ideal state of logs should be this:
Ideal State of terminal logs
❯ make run.all
Untagged: bitespeed-backend-task-app:latest
Deleted: sha256:e248469c86219cf1f8f729d521fd69e263e16134630b7113949b31c02c263b35
[+] Building 16.6s (17/17) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 989B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 0.7s
=> [internal] load metadata for docker.io/library/golang:1.20-alpine 0.7s
=> [build 1/6] FROM docker.io/library/golang:1.20-alpine@sha256:fd9d9d7194ec40a9a6ae89fcaef3e47c47de7746dd5848ab5343695dbbd09f8c 0.0s
=> [stage-1 1/5] FROM docker.io/library/alpine:latest@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 14.45kB 0.0s
=> CACHED [build 2/6] RUN apk add --no-cache tzdata 0.0s
=> CACHED [build 3/6] WORKDIR /app 0.0s
=> [build 4/6] COPY . /app 0.0s
=> [build 5/6] RUN go mod download 6.0s
=> [build 6/6] RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o bite ./cmd/ 9.7s
=> CACHED [stage-1 2/5] RUN apk add --no-cache tzdata 0.0s
=> CACHED [stage-1 3/5] WORKDIR /app/ 0.0s
=> CACHED [stage-1 4/5] COPY --from=build /app/bite /app/bite 0.0s
=> CACHED [stage-1 5/5] COPY --from=build /app/config /app/config 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:e248469c86219cf1f8f729d521fd69e263e16134630b7113949b31c02c263b35 0.0s
=> => naming to docker.io/library/bitespeed-backend-task-app 0.0s
[+] Running 2/2
✔ Container bitespeed-backend-task-db-1 Recreated 0.0s
✔ Container bitespeed-backend-task-app-1 Recreated 0.0s
Attaching to bitespeed-backend-task-app-1, bitespeed-backend-task-db-1
bitespeed-backend-task-db-1 |
bitespeed-backend-task-db-1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
bitespeed-backend-task-db-1 |
bitespeed-backend-task-db-1 | 2023-07-04 13:41:24.359 UTC [1] LOG: starting PostgreSQL 15.3 (Debian 15.3-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
bitespeed-backend-task-db-1 | 2023-07-04 13:41:24.360 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
bitespeed-backend-task-db-1 | 2023-07-04 13:41:24.360 UTC [1] LOG: listening on IPv6 address "::", port 5432
bitespeed-backend-task-db-1 | 2023-07-04 13:41:24.362 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
bitespeed-backend-task-db-1 | 2023-07-04 13:41:24.365 UTC [29] LOG: database system was shut down at 2023-07-04 13:41:05 UTC
bitespeed-backend-task-db-1 | 2023-07-04 13:41:24.369 UTC [1] LOG: database system is ready to accept connections
bitespeed-backend-task-app-1 |
bitespeed-backend-task-app-1 | _______ ___ _______ _______ _______ _______ _______ _______ ______
bitespeed-backend-task-app-1 | | _ || || || || || || || || |
bitespeed-backend-task-app-1 | | |_| || ||_ _|| ___|| _____|| _ || ___|| ___|| _ |
bitespeed-backend-task-app-1 | | || | | | | |___ | |_____ | |_| || |___ | |___ | | | |
bitespeed-backend-task-app-1 | | _ | | | | | | ___||_____ || ___|| ___|| ___|| |_| |
bitespeed-backend-task-app-1 | | |_| || | | | | |___ _____| || | | |___ | |___ | |
bitespeed-backend-task-app-1 | |_______||___| |___| |_______||_______||___| |_______||_______||______|
bitespeed-backend-task-app-1 |
bitespeed-backend-task-app-1 |
bitespeed-backend-task-app-1 | GoVersion: go1.20.5
bitespeed-backend-task-app-1 | GOOS: linux
bitespeed-backend-task-app-1 | GOARCH: amd64
bitespeed-backend-task-app-1 | NumCPU: 12
bitespeed-backend-task-app-1 | GOROOT: /usr/local/go
bitespeed-backend-task-app-1 | Compiler: gc
bitespeed-backend-task-app-1 | Compiler: Tuesday, 4 Jul 2023
bitespeed-backend-task-app-1 | -----------------
bitespeed-backend-task-app-1 | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
bitespeed-backend-task-app-1 |
bitespeed-backend-task-app-1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
bitespeed-backend-task-app-1 | - using env: export GIN_MODE=release
bitespeed-backend-task-app-1 | - using code: gin.SetMode(gin.ReleaseMode)
bitespeed-backend-task-app-1 |
bitespeed-backend-task-app-1 | [GIN-debug] POST /identify --> github.com/AadumKhor/bitespeed-backend-task/src/app/handlers.IdentifyHandler.Handle-fm (4 handlers)
bitespeed-backend-task-app-1 | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
bitespeed-backend-task-app-1 | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
bitespeed-backend-task-app-1 | [GIN-debug] Listening and serving HTTP on :8080
If it is any different, please reach out. It might be an issue that I have missed.
If you are using data from the problem doc itself the following cURL requests can be used.
curl --location 'http://localhost:8080/identify' \
--header 'Content-type: application/json' \
--data-raw '{
"email": "mcfly@hillvalley.edu",
"phoneNumber": 123456
}' | jq |
{
"contact": {
"emails": ["lorraine@hillvalley.edu", "mcfly@hillvalley.edu"],
"phoneNumbers": ["123456"],
"primaryContactId": 1,
"secondaryContactIds": [2]
}
} |
curl --location 'http://localhost:8080/identify' \
--header 'Content-type: application/json' \
--data '{
"phoneNumber": 123456
}' | jq |
{
"contact": {
"emails": ["lorraine@hillvalley.edu", "mcfly@hillvalley.edu"],
"phoneNumbers": ["123456"],
"primaryContactId": 1,
"secondaryContactIds": [2]
}
} |
curl --location 'http://localhost:8080/identify' \
--header 'Content-type: application/json' \
--data-raw '{
"email": "random@hillvalley.edu",
"phoneNumber": 9988776655
}' | jq |
{
"contact": {
"emails": [
"random@hillvalley.edu"
],
"phoneNumbers": [
"9988776655"
],
"primaryContactId": 8,
"secondaryContactIds": []
}
} |
Modify the above given cURL requests to add your own data. I have used cURL but you can import any one of these in Postman and play with the API!