fintual-backend-devops-go
README.md

Content service in Go

A Go implementation of the Backend/DevOps interview service, backed by PostgreSQL. It serves users, posts, comments, and tags. The original Django application remains in the repository as a reference for behavior tests and performance comparisons.

See assignment notes for the decisions and measured comparison for results and limitations. The original brief and Django setup instructions are in ASSIGNMENT.md.

Run locally

Requirements:

  • Go 1.26 or later. Tested with Go 1.26.4.
  • A running PostgreSQL server and its command-line tools, including psql. Tested with PostgreSQL 17.10.
  • Bash.

There are two binaries:

  • go-service/contentd serves the API and applies schema migrations. This is the production binary.
  • harness/harness creates dedicated test databases, loads test data, verifies behavior, and runs benchmarks. It is not needed in production.

Build both from the repository root:

bin/build

Both binaries default to the assignment_harness_native database. Configure the allowed HTTP hosts in your shell:

export ALLOWED_HOSTS='127.0.0.1,localhost'

This connection uses a local PostgreSQL socket and your OS username. That PostgreSQL role must exist and have permission to create databases. For a different connection, set PGHOST, PGPORT, and PGUSER, or include them in DATABASE_URL:

export DATABASE_URL='host=localhost port=5432 user=postgres dbname=assignment_harness_native'

Use a local .pgpass file with mode 0600 for passwords.

Initialize the development database and load the small sample:

harness/harness init-db
go-service/contentd migrate
harness/harness claim
harness/harness fixture

All these commands use DATABASE_URL when set, otherwise the default database. init-db creates the database, migrate applies the service's embedded schema, and claim marks the empty database as owned by the test harness. Harness database commands require a dedicated database whose name starts with assignment_harness_; they refuse to take over an existing unmarked database automatically.

Start the server:

go-service/contentd

The server uses production settings by default, including health endpoints and host validation. In another terminal:

curl http://127.0.0.1:18001/readyz
curl http://127.0.0.1:18001/api/posts

Stop with Ctrl+C. To restart, set ALLOWED_HOSTS in your shell and run go-service/contentd again. No initialization or data loading is needed. Set LISTEN_ADDR to change the default 127.0.0.1:18001 bind address. Go development and operation do not require Python, uv, mise, or Docker.

CLI help

Every binary and bin/ script supports --help and -h. Help exits without connecting to PostgreSQL, loading data, building binaries, or starting a server.

go-service/contentd --help
go-service/contentd migrate --help
harness/harness --help
harness/harness seed --help
harness/harness bench --help
bin/check --help

Harness command help lists only relevant flags, their defaults, an example, and whether the command replaces data or output files. harness/harness help seed also works. Override the default connection with DATABASE_URL, or pass --db to the harness explicitly.

Fixture data

Choose the dataset with the harness, using the same DATABASE_URL as the server:

# Small contract-test sample: 3 users, 3 tags, 4 posts, 3 comments.
# GET /api/posts returns the 3 published posts; the fourth is a draft.
harness/harness fixture

# Full synthetic dataset: 1,000 users, 50 tags, 100,000 posts, 500,000 comments.
harness/harness seed

Run only the loader for the dataset you want. Both commands accept an explicit --db 'dbname=assignment_harness_native' instead of DATABASE_URL. Connection precedence is --db for the harness, then DATABASE_URL, then dbname=assignment_harness_native. An unset or empty DATABASE_URL uses the default.

Both loaders replace existing application data. Use them only with owned, marked test databases. No server restart is needed after loading data. /api/posts is unpaginated, so the full dataset produces a large response.

API

Method Path Description
GET /api/posts Published posts, newest first
GET /api/posts/search?q= Search across title and body
GET /api/posts/by-tag/{slug} Posts carrying a given tag
GET /api/posts/{id} Post detail with comments
POST /api/posts Create a post
POST /api/posts/{id}/comments Add a comment to a post
GET /api/users/{id} User profile with post and comment counts
GET /api/users/find?email= Look up a user by email

Interactive API docs are at http://127.0.0.1:18001/api/docs, with the schema at /api/openapi.json.

With the small fixture loaded, try these commands in another terminal:

curl 'http://127.0.0.1:18001/api/posts/search?q=python'
curl http://127.0.0.1:18001/api/posts/by-tag/python
curl http://127.0.0.1:18001/api/users/1
curl 'http://127.0.0.1:18001/api/users/find?email=alice@example.com'
curl -X POST http://127.0.0.1:18001/api/posts \
  -H 'Content-Type: application/json' \
  -d '{"author_id":1,"title":"My first post","body":"Hello from Go","tag_slugs":["python"]}'
curl -X POST http://127.0.0.1:18001/api/posts/1/comments \
  -H 'Content-Type: application/json' \
  -d '{"author_id":2,"body":"A new comment"}'

Post creation requires author_id, title, and body; tag_slugs is optional. Authors and tags must already exist. The API has no user or tag creation endpoint. The full seed uses different users and titles from the small fixture.

The implementation preserves the reference API's observed behavior, including known correctness limitations. Authentication and pagination are not implemented.

Checks

bin/check

The full check suite additionally requires Python 3.14, uv, and curl because it runs the Django reference and optimized Django control alongside Go. The inherited mise.toml can install Python and uv if you use mise; mise is optional.

Checks include Go unit tests, race and static checks, the original smoke tests, migrations, production behavior, and 66 contract cases. They reset dedicated test databases and temporarily use ports 18000 and 18001, so stop the development server first. See DEVELOPMENT.md for details and benchmark commands.

Troubleshooting

Symptom Next step
Socket connection fails Start PostgreSQL, or set PGHOST and PGPORT for your server.
Role does not exist or peer authentication fails Use a PostgreSQL role that exists and matches your connection's authentication rules; set PGUSER or user= in DATABASE_URL.
Permission denied to create database Ask the database administrator to create a dedicated database owned by your role, then use the manual setup in DEVELOPMENT.md.
Existing database is unmarked Choose a fresh assignment_harness_* database name, or use the manual procedure for an empty database.
Address already in use Stop the previous server or set LISTEN_ADDR=127.0.0.1:18002 and use that port in requests.
/readyz returns 404 Health endpoints require APP_PROFILE=production, the default; check that you have not set APP_PROFILE=compat.

Production operation

contentd defaults to production mode. It requires ALLOWED_HOSTS and an explicitly migrated database. Set DATABASE_URL to your production database connection; otherwise it uses assignment_harness_native. Production mode adds health endpoints, request limits, structured logs, and generic error responses.

For reference behavior tests only, start the same binary in compatibility mode:

APP_PROFILE=compat go-service/contentd

Stop the previous server first. Compatibility mode preserves legacy error responses and does not expose health endpoints.

Deployment uses the compiled contentd binary and PostgreSQL. Follow deploy/README.md for database roles, explicit migrations, environment configuration, the example systemd unit, shutdown, and rollback. The development setup and fixture loaders are not a production provisioning flow.