This repository contains a demo backend server application for managing patient records.
- It implements the following REST API:
Method | URL | Description | Auth-protected |
---|---|---|---|
GET | /health | Get service health | No |
GET | /generate-token | Generate a valid JWT token for the API calls | No |
GET | /api/v1/patients | Get all patients | Yes |
GET | /api/v1/patients/:id | Get one patient | Yes |
POST | /api/v1/patients | Add one patient | Yes |
-
The /api endpoint is auth-protected via JWT tokens.
-
The request and response bodies are in JSON format
-
It uses an exponential backoff algorithm for establishing the database connection in case it takes a while for the database to come online or if connecting to it is slow.
-
It uses docker healthchecks
-
It injects the current version into the executable binary at build time and it reports it via the health check.
-
It produces a single static binary that is stored in an Alpine Linux docker container.
-
It has unit and integration tests (see http_test.go and integration_test.sh).
-
It has an MIT LICENSE
-
It follows the Twelve-Factor App guidelines:
- Codebase: One version-controlled codebase in this repo.
- Dependencies: Its direct 3rd party dependencies are listed in go.mod.
- Config: Its configuration is done through environment variables (see the configuration section below).
- Backing services: This app requires access to a Postgres database, to which it connects via the configured. parameters. Both this app and the database can be launched via docker-compose.
- Build, release, run: Out of scope for this demo, but I could, for example, use GitHub Actions to build a CI/CD pipeline which triggers on every pull request, builds a new version of the docker container and releases the new version into a cloud-based test environment, for example on Google Cloud Platform, once the pull request is merged.
- Processes: The app is executed as one or more stateless processes. Since the patient data is stored in a database, multiple instances of this app can be deployed simultaneously to offer high availability and horizontal scaling. Some caching layer(s) should be considered, depending on the workflows and traffic.
- Port binding: The app exposes its HTTP API through an embedded HTTP server which listens on a configurable port.
- Concurrency: As mentioned under 6., this app can be scaled out by running multiple instances of it simultaneosly.
- Disposability: The Go runtime is quite fast, so this app usually starts up in less than a second if the database is running and accepting connections immediately. This app is also designed to shut down gracefully. When it receives either SIGINT or SIGTERM, it rejects any new incoming connections and waits for the connections that are currently in flight to finish before shutting down (see main.go for details).
- Dev/prod parity: Out of scope for this demo, but as mentioned under 5., multiple cloud-based environments can be provisioned for this app and have the CI/CD pipeline trigger automatic deploys to each of them as needed.
- Logs: This app emits logs on STDOUT which get collected by docker in the docker logs. A system such as logspot can be put in place to collect these logs and send them to a centralised aggregator, or, as is popular nowadays, a 3rd party cloud-based log management system can be used (for example Sumo Logic), which provides a log collector that is capable of picking up all docker logs and streaming them directly to their cloud for storage and aggregation.
- Admin processes: This app doesn't require such processes and it uses the docker Alpine Linux container as a base
layer which contains the
ferrum
server binary. It provides various tools for debugging and troubleshooting issues, most of which needing to be installed before they can be used. I could also use the dockerscratch
container as a base layer instead of Alpine Linux, but this doesn't havewget
in it (because it's empty), which is needed by the docker healthcheck to determine if theferrum
process is up and running.
Please note that I have tested this app only on OSX, but I expect it to work just fine on Linux as well.
It is assumed that you have Go 1.14+ and Docker 2.2.0.5+ installed locally and configured correctly.
> # A few 3rd party Go dependencies need to be installed first.
> make install-deps
> # If desired, the database layer code generator can be executed again.
> # I have committed the output in git, since the files are quite small.
> make generate
> # Run linters on the code to make sure everything is sane.
> make lint
> # Run the unit tests.
> make test
> # Build the actual executable if you want to try launching it directly from
> # your console.
> # This will create an executable called `ferrum` in the root directory of the repo.
> make build
> # Build the `mihaitodor/ferrum` docker image.
> # (or you can let docker-compose build it for you when it starts up)
> make docker
> # Launch the `mihaitodor/ferrum` and `postgres` containers via docker-compose.
> make up
> # Feel free to use `docker ps` to observe the container healthchecks turn healthy.
> docker ps
> # Stop docker-compose and tear down the containers once you're finished with them.
> make down
> # Run the integration tests.
> # This will run a hopefully-not-too-ugly bash script (integration_test.sh).
> make test-integration
FERRUM_DATABASE_HOST
: The host for the database server (defaultlocalhost
)FERRUM_DATABASE_PORT
: The port for the database server (default5432
)FERRUM_DATABASE_USER
: The user for the database server (defaultpostgres
)FERRUM_DATABASE_PASSWORD
: The password for the database server (defaultpostgres
)FERRUM_DATABASE_NAME
: The database name (defaultferrum
)FERRUM_HTTP_API_PORT
: The embedded HTTP server port (default80
)FERRUM_HTTP_REQUEST_TIMEOUT
: The maximum HTTP request timeout (default3s
)FERRUM_HTTP_MAX_POST_SIZE
: The maximum POST request content size (default1MiB
)FERRUM_HTTP_JWT_SIGNING_KEY
: The JWT token signing key (defaultdeadbeef
)FERRUM_HTTP_JWT_CLAIM_NAME
: The JWT token claim name (defaultferrum
)FERRUM_HTTP_JWT_EXPIRATION
: The JWT token expiration (default1h
)FERRUM_LOG_LEVEL
: The logging level (defaultinfo
)
This app listens on port 80 by default, which is also the port exposed by the docker container.
Go produces very efficient code with static binaries and very fast runtime. It does, however have a lightweight garbage collector, which places it somewhere in between C++ / Rust and Java / C# in terms of raw performance. Considering the difficulty of writing such a service in a language like C++, Go is highly suitable for developing small web microservices and, alongside the generous Go standard library, there exists a plethora of open source libraries and frameworks which enable developing such services quickly and efficiently.
Happy to drill deeper into any of these and maybe even implement some if you'd like me to. I just need more time.
-
Add more unit tests to have better coverage for corner cases and some of the code paths which aren't currently being exercised.
-
I'd write the integration tests in Python, or, if this app becomes more complex and requires more dependencies, I would investigate using a framework such as testcontainers-go.
-
Add performance unit tests and perform some load testing using a tool such as hey or locust.
-
Deploying to a cloud environment: While outside of the scope of this assignment, I'm happy to write some Terraform code which can orchestrate the deployment of this service in a cloud provider (prefferably AWS, since I have extensive experience with it).
-
Monitoring: While outside of the scope of this assignment, monitoring is a very important topic. For production readiness, one open source possibility is to embed the Prometheus agent into the code and have it send metrics to a Prometheus server. Then, for example using Grafana I'd implement some charts which show several key metrics over time: CPU and memory usage, various HTTP status counts, container restarts (assuming we're running multiple instances), request latencies (median for each relevant percentile), garbage collector stats, etc.
-
Caching: Depending on the intended usage and performance requirements, some of the lookup results from the database can be cached either in memory or on disk or via a separate service.
-
Use a process manager in the container, such as s6 In some cases, the docker container can be heavyweight and it might also keep some state, such as a large cache, in which case it can be desirable to have a process manager restart the app immediately inside the container in case it dies for whatever reason.
-
Run the process in the container as a regular user instead of
root
. There are some valid security concerns with allowing processes to run as root in docker containers. Usingscratch
as a base layer makes it difficult to run as non-root, but Alpine Linux is a very small image, so this can be a good tradeoff if this configuration is mandated. -
Implement CORS properly. Adding one patient via the
/api/v1/patients
endpoint requires a preflightOPTIONS
request before the actualPOST
request is issued when run from a browser. This requires special handling of certain headers, which I have implemented, but it's probably insecure. This needs to be reviewed in detail. -
Rate limiting might be nice to have, although that can be achieved via the service mesh proxies.
-
The healthcheck should be enhanced and perhaps split up into two separate probes:
livenessProbe
andreadinessProbe
. This would be useful if this app will be run in a Kubernetes cluster because K8s will be able to make better scheduling decisions if thelivenessProbe
reports healthy as soon as the process is running, while it waits for thereadinessProbe
to report OK after the connection to the database has been established. -
Investigate what happens when the database connection drops. Does it reconnect automatically?
-
Mask secrets when dumping the configuration via rubberneck.
-
Input data validation. There is some implicit validation being done, by the JSON serialisation / deserialisation and by the database engine, but more is required.