Horizon Social Network - Microservices Backend
Overview
What it is
Horizon Social Network is a Go backend for a campus social platform. It has two services: Posts stores user posts in MongoDB, and Followers stores the social graph in Neo4j.
Both services run on Kubernetes behind Istio. Keycloak handles identity, the Istio ingress gateway validates JWTs, and the mesh encrypts service-to-service traffic with mTLS.
Why it exists
I built it as part of a university workshop project to apply microservices and Kubernetes concepts in practice. I treated it like a production backend: posts and follower relationships have different data shapes, so I split them into separate services with separate databases instead of forcing both workloads into one model.
The workshop gave me room to apply the parts I care about as a cloud and DevOps engineer: container builds, Kubernetes deployment, service mesh policy, auth at the edge, and clear service boundaries.
Outcome
- Two Go services with handler, service, and repository layers
- MongoDB stores post documents and timeline queries
- Neo4j stores
Usernodes andFOLLOWSrelationships - Istio validates Keycloak JWTs at ingress and enforces mTLS in the mesh
- Multi-stage Docker builds produce distroless images under 10MB per service
- Kubernetes manifests cover deployments, services, config, secrets, and Istio routing
Architecture
High-Level Flow
The client enters through the Istio ingress gateway. Istio validates the Keycloak JWT before the request reaches a service, then routes traffic through the mesh. Posts reads and writes MongoDB documents. Followers handles graph queries in Neo4j.
Tech Stack
Backend: Go, Gin
Databases: MongoDB, Neo4j
Service Mesh: Istio
Authentication: Keycloak
Container & Orchestration: Docker, Kubernetes
Build Tools: Make
Implementation Setup
Service Layout
Each service follows the same Go structure: handler -> service -> repository.
Posts Service:
- CRUD for user posts
- Timeline queries by
user_id - MongoDB repository with an index on
user_id - OpenAPI spec at
api/openapi/posts.json
Followers Service:
- Follow and unfollow operations
- Followers and following lookups
- Friend suggestions through graph traversal
- Neo4j repository with Cypher queries
- OpenAPI spec at
api/openapi/followers.json
Project Layout:
cmd/: service entry points forpostsandfollowersapi/: handlers, models, repositories, services, and routersconfig/: per-service configurationcommon/: shared loggerMakefile: build, lint, format, Docker targets, and cleanup
Docker Builds
One Dockerfile builds both services through build args:
FROM golang:1.23.2 AS builder
ARG SERVICE
WORKDIR /app
COPY . .
ENV CGO_ENABLED=0
RUN go mod download
RUN go build -o app -ldflags="-s -w" ./cmd/${SERVICE}/main.go
FROM gcr.io/distroless/static-debian12 AS runtime
ARG PORT
COPY --from=builder ./app ./
EXPOSE ${PORT}/tcp
ENV GIN_MODE=release
ENTRYPOINT ["./app"]
Build choices:
- Builder stage keeps the Go toolchain out of the runtime image
CGO_ENABLED=0creates a static binary-ldflags="-s -w"strips debug data and the symbol table- Distroless runtime removes shell and package manager surface area
SERVICEandPORTchoose the target service at build time
Database Design
MongoDB for Posts:
postscollection withuser_id,content,created_at, andupdated_atuser_idindex for timeline reads- Document structure leaves room for post fields such as media, reactions, or visibility settings
Neo4j for Followers:
Usernodes withuser_idandusername- Directional
FOLLOWSrelationships - Cypher queries for followers, following, and mutual-connection suggestions
Example queries:
- Followers:
MATCH (follower)-[:FOLLOWS]->(user) WHERE user.user_id = $id - Following:
MATCH (user)-[:FOLLOWS]->(following) WHERE user.user_id = $id - Suggestions: traverse mutual connections and exclude users already followed
Build System
The Makefile keeps the local loop short:
make build SERVICE=posts
make build-container SERVICE=followers PORT=8081
make run SERVICE=posts
make lint
make fmt
make clean
Istio and Keycloak
mTLS:
- Istio enforces mutual TLS between services
- Istio handles certificate rotation
- Services communicate over in-mesh ClusterIP routes
Authentication:
RequestAuthenticationpoints to the Keycloak JWKS endpointAuthorizationPolicyrejects unauthenticated requests at ingress- Services receive validated identity headers instead of parsing JWTs themselves
Traffic policy:
- Load balancing across replicas
- Timeouts for service calls
- Retry and circuit-breaking policy at the mesh layer
Kubernetes Deployment
- One Deployment per service
- ClusterIP Services for in-mesh traffic
- Istio
VirtualServicefor external routes - ConfigMaps for service URLs and non-secret connection settings
- Secrets for MongoDB and Neo4j credentials
- Environment variables for ports, database URIs, and service URLs
Service configuration:
- Posts:
POSTS_PORT,MONGO_URI,MONGO_DB,FOLLOWERS_SERVICE_URL - Followers:
FOLLOWERS_PORT,NEO4J_URI,NEO4J_DB,NEO4J_USER,NEO4J_PASSWORD,POSTS_SERVICE_URL
Key Challenges & Solutions
Challenge 1: Choosing the Right Database Per Workload
Problem: Posts fit a document model. Followers fit a graph model. Putting both in one database would either complicate timeline reads or push graph traversal into application code.
Solution: I split the backend by workload. Posts uses MongoDB for document storage and indexed timeline reads. Followers uses Neo4j for relationship traversal and friend suggestions. When a feature needs both sides, one service calls the other over REST.
Each service uses a database that matches its query pattern. Friend suggestions stay in Cypher instead of turning into nested application-side joins.
Challenge 2: Keeping Auth Out of Each Service
Problem: Adding JWT parsing to each service would duplicate security code. A third service would need the same middleware again.
Solution: I moved authentication to Istio. The ingress gateway validates Keycloak JWTs through RequestAuthentication, and AuthorizationPolicy blocks unauthenticated traffic before it reaches Go. Services read the identity headers Istio passes through.
The Go services do not carry JWT parsing code. A new service can join the mesh and inherit the same auth policy.
Challenge 3: Shrinking Runtime Images
Problem: A Go binary on a standard Linux base image produced images over 100MB. That slowed image pulls and shipped packages the services did not need.
Solution: I used multi-stage builds. The builder image compiles a static Go binary, and the runtime image uses gcr.io/distroless/static-debian12. The final image contains the binary and little else.
Each service image dropped below 10MB, about a 90% reduction from the distro-base build.