Summary

This project was made especially as a requirement for passing the technical test in Spenmo. It is a simple golang application that allows us to create a team/individual wallet where each wallet may has multiple cards.

In the docs folder, you can find the schema of the database and the api collection (I use Insomnia) for the project.

Constraints and Assumptions

  • Client makes a read or write request
  • Service does processing, stores data, then returns the results
  • Service needs to evolve from serving a small amount of users to a lot of users
  • Service has high availability

System Design Plan

The project is written in golang and uses the following technologies:

  • Gokit (a toolkit for microservices)
  • PostgreSQL (database)

The uses of Gokit was decided since its design is well suited with modern software design like elegant monolith and microservices. Aside from that, it is also a toolkit that I am familiar with. Since Gokit also support the separation of concerns, the project can be easily divided into multiple layers based on the Service Layer pattern. The project is divided into the following layer groups:

  • Transport layer, which is responsible for the communication between the application and the rest of the internet (HTTP, Grpc, etc).
  • Endpoint layer, which is responsible in implementing the transport into a business logic, with endpoint we can have two different transport to access a specific business logic.
  • Service layer, which is responsible for the business logic.

The implementation of all those layers abide to the Clean Code architecture and Hexagonal architecture.

Starts As Monolith

For the sake of simplicity, the project is assumed to be a monolith. This means that we can run the project as a single service, either as a static binary in a vps or as a container in docker. We can even put the database in a similar box.

When we need to scale it, we can do vertical scalling by adding more power. Since the resources are limited (a single box), we can easily monitor the metrics of the box to determine bottlenecks.

Starts As Microservice

Once the users grow, we can deploy each part of the project as a microservice. We can start scaling each service horizontally based on its traffic. Deploying the services as containers will make the scaling easier.

We separate each table on the previous database into a separate database. Each service will own its own database and other services cannot directly access the database. Each service can also be splitted into multiple services based on its task, read or write (CQRS pattern). This will allow us to use a different database between the read (replica) and write (master) services.

Since the project has grown into multiple services, we also need to make sure the services can only be accessed from a single gateway and use a private subnet for everything else. We can use an API Gateway for this.

Observability Plan

If money is not the problem and we don’t have enough resources to setup a monitoring system, we can just use a hosted monitoring system like Datadog or New Relic.

We can also use a local monitoring system like Prometheus or InfluxDB. Based on the comparison of the monitoring system on the Prometheus page, I will choose Prometheus instead of InfluxDB. The reason is that I prefer to use another tool for logging like ELK instead of using InfluxDB for both use cases. Quoted from the Prometheus page, Prometheus has “more powerful query language, alerting, and notification functionality”. It also has “higher availability and uptime for graphing and alerting”.

Based on above use case, I already included a requestId metadata on each request that comes in and comes out into/from the service. We can just use it to trace and/or identify the request between all related microservices.

An example of prometheus implementation can be seen by accessing "/metrics" endpoint.

Relational Tables

Relational Tables

Setup database

Make sure the postgres has been up and running. If it is not, we can run it in the docker:

docker run --name postgres-svc-spenmo -p 5432:5432 \
    -e POSTGRES_DB=spenmo \
    -e POSTGRES_USER=user \
    -e POSTGRES_PASSWORD=password \
    -v ~/Tmp/postgres_data:/var/lib/postgresql/data \
    -d postgres:13.2-alpine

or

docker start <container_id>

Install golang-migrate:

brew install golang-migrate

Migrate the current schema:

export POSTGRESQL_URL='postgres://user:pa[email protected]:5432/spenmo?sslmode=disable'
migrate -database ${POSTGRESQL_URL} -path config/db/migrations up

Common error:

error: Dirty database version 2. Fix and force version.

Solution:

migrate -database ${POSTGRESQL_URL} -path config/db/migrations force <version - 1>

Run the app

Use Makefile with hot reload enabled (air will automatically installed):

make watch

Use Makefile with hot reload disabled:

make run

Run the main go file:

go run cmd/main.go

For other options, we can use make help:

❯ make help

Usage:
  make <target>

Targets:
  build              Build your project and put the output binary in build/spenmo-test
  clean              Remove build related file
  docker-build       Use the dockerfile to build the container (name: spenmo-test)
  docker-release     Release the container "spenmo-test" with tag latest and 0.0.1
  help               Show this help message
  lint               Run all available linters
  lint-dockerfile    Lint the Dockerfile using 'hadolint/hadolint'
  lint-go            Lint all go files using 'golangci/golangci-lint'
  test               Run the tests of the project
  test-unit          Run the unit tests of the project
  test-integration   Run the integration tests of the project
  vendor             Copy all packages needed to support builds and tests into the vendor directory
  watch              Run the code with 'cosmtrek/air' to have automatic reload on changes

Testing

Unfortunately, in this current version, I am only able to finish the integration part of the testing that will test the service part of the project. That’s why, the only working Make target is test-integration. Since the only working test is the integration test, I didn’t include the test in the Gitlab CI yaml file (it needs a working PostgreSQL). Below is the command to trigger the test and the current status of the test:

❯ make test-integration
go test -race -run ".Integration" ./...
?       gitlab.com/renodesper/spenmo-test/cmd                             [no test files]
?       gitlab.com/renodesper/spenmo-test/endpoint                        [no test files]
?       gitlab.com/renodesper/spenmo-test/middleware                      [no test files]
?       gitlab.com/renodesper/spenmo-test/middleware/recover              [no test files]
?       gitlab.com/renodesper/spenmo-test/repository                      [no test files]
?       gitlab.com/renodesper/spenmo-test/repository/postgre              [no test files]
ok      gitlab.com/renodesper/spenmo-test/service               (cached)
ok      gitlab.com/renodesper/spenmo-test/transport/http        (cached)  [no tests to run]
?       gitlab.com/renodesper/spenmo-test/util/card                       [no test files]
?       gitlab.com/renodesper/spenmo-test/util/constant                   [no test files]
?       gitlab.com/renodesper/spenmo-test/util/ctx                        [no test files]
?       gitlab.com/renodesper/spenmo-test/util/error                      [no test files]
?       gitlab.com/renodesper/spenmo-test/util/errors                     [no test files]
?       gitlab.com/renodesper/spenmo-test/util/logger                     [no test files]
?       gitlab.com/renodesper/spenmo-test/util/logger/noop                [no test files]
?       gitlab.com/renodesper/spenmo-test/util/logger/zap                 [no test files]
?       gitlab.com/renodesper/spenmo-test/util/response                   [no test files]

GitHub

View Github