justin hofmann

Platform & Backend Engineer · Remote · Germany

I build the platform behind smart city bollards.

For eight years at GEMTEC I've built and operated an on-prem microservice platform that lets cities control physical infrastructure: retractable bollards driven by RFID, phone calls, and time-boxed access permissions, with a full audit history — running in around ten German cities.

I own the parts that can destroy a customer installation: the API gateway, the deployment layer, backup & restore, and in-place updates. My work is orchestration and integration on systems that share no transactions and fail in interesting ways.

1,193commits across the platform
45repositories contributed to
108/108commits on the deployment layer — sole author
63merge requests reviewed, across 18 repos
~10cities running the platform

Every number on this page comes straight from the git record. Ask me about any of them.

Systems I own

OpenResty API gateway

2021 → today · 108/222 commits
principal author, 5 years

Every request in the product passes through it. I chose OpenResty over HAProxy, built JWT verification in Lua, and grew it from a router into an application platform: CRUD services inside the gateway, a restore mode that blocks writes while databases are down, and a login-less status page for the moments when authentication itself is deliberately offline.

Backup & restore

2025–2026 · built solo
in production since 11/2025

A saga orchestrator that coordinates backup and restore across six stateful systems — Postgres, Graylog/MongoDB, OpenSearch, Node-RED, gateway config — that share no transactions. Fourteen sequenced restore steps with reverse-order rollback; restores onto different machines and onto newer product versions, migrating data between changed schemas on the way. Failed migrations can't touch live data: everything restores into throwaway databases first, then gets promoted.

It also streams. I replaced read-then-write with streaming I/O so that large restores would stop exhausting memory — and was genuinely surprised to find it had made them 50–100× faster as well.

In-place updater

designed 2024 · built 2026
GraalVM native, boots in ~50 ms

A native-compiled Quarkus service that updates the entire on-prem stack, including the Proxmox VMs it runs on — and including itself, via a versioned handoff to its own successor. Readiness is a gate, not a check: the whole system has to serve before an update commits, and a rolled-back update reports as failed, never as done.

The deployment underneath it is blue-green on Proxmox: two VMs sharing NFS storage, only one ever powered on, so the installation keeps a single IP and MAC and needs no load balancer in front of it. Doing that on one Proxmox host is easy; the work is moving an installation between hosts with different names and missing ISOs.

Deployment layer

2021–2022 · 108/108 commits
then deleted — deliberately

I wrote the platform's entire Nomad/Consul deployment as an apprentice: a job spec per service, dependency ordering, memory oversubscription, the installer. Two years later I argued for replacing it with Docker Compose and drove that migration. The story of why is below — it's the best decision I've made.

Frontend platform

10 applications · shared library chain
Vue 2 → Vue 3, estate-wide

I own the shared library chain every frontend depends on, and I led the estate-wide Vue 2 → Vue 3 migration — Webpack to Vite, state management to Pinia — re-engineering the build stack across ten applications to make it possible.

Review & releases

63 MRs across 18 repositories
reviewer for cross-cutting change

When one architectural pattern has to land consistently across eight backends, I'm the person who checks that it did — contract gaps, duplicate migrations, a global mutex held across network calls. I also reverse-engineered our previously undocumented 18-repository release process from git history alone and documented it, dependency order and all.

Three stories

on judgement

The best code I ever wrote is code I deleted.

The Nomad deployment layer was mine alone — 108 commits, not one by anyone else. I liked it. It worked. And that was exactly the problem: I was the only person in the company who could operate it. A deployment stack with a bus factor of one isn't infrastructure, it's a liability with my name on it.

So I argued for replacing roughly a year of my own work with Docker Compose — simpler, boring, and something the whole team could actually run. We did, and that Compose deployment still ships to every customer today. I learned more about architecture from deleting that layer than from building it: the sophisticated option you personally prefer is not the same thing as the right option.

on maintainability

I rewrote a system that already worked.

Backup/restore v2 was shipped and running at customers. It also had two god controllers and a complexity curve I could feel bending the wrong way. I tried a safety net first — an availability gate that locks the feature out when preconditions fail — and realized it treated the symptom, not the cause. I brought in a senior colleague for a sanity check, then rewrote the core onto a saga architecture: explicit steps, compensating rollback in reverse order, state carried in a context object because mid-restore the database you'd read it from is down.

Three weeks, ~13,000 lines deleted, ~16,000 written. The hard part wasn't the code — it was deciding that "it works" and "it's finished" are different claims.

on stubbornness

Six thousand classes stood between us and a native build.

Our telephony driver was the last backend that couldn't compile to a GraalVM native image. The blocker: its ARI library negotiates the protocol version at runtime by reflectively probing all 31 generated implementation packages — about 6,000 classes. Fine on the JVM, impossible in a native image.

The fix was to make the version configurable without changing JVM behavior by a single bit, generate reflection metadata for the five versions we actually support, and document how to extend it with the tracing agent. Boot time went from JVM startup to ~50 ms and memory from ~350 MB to ~100 MB. Along the way I found the integration tests' dialplan had been silently ignored for years — the config file was named extension.conf, and Asterisk reads extensions.conf.

Skills

Backend
Java (6 y) with Quarkus — Jakarta EE/MicroProfile, directly transferable to Spring Boot (I'm porting a service to Spring Boot right now). REST API design & versioning, saga/orchestration patterns, GraalVM native images, SSE, resumable uploads, JWT auth.
Infrastructure
Docker & Compose, NGINX/OpenResty + Lua, GitLab CI, HashiCorp Nomad & Consul, AWS (ECS/ECR/RDS/IAM — one full product deployment), release engineering, Proxmox, Postgres operations, Graylog/OpenSearch.
Frontend
Vue 2/3 + Vuetify, TypeScript, Pinia — five years across ten applications and the shared component library underneath them.
Devices & protocols
MQTT design and implementation, binary device protocols, Node-RED, Asterisk/ARI. Domain experience across bollards, access control, fire alarm, nurse call, and PLCs — software that moves physical steel.

Off the clock

centurio-mqtt

A Go bridge from my home battery/inverter system to MQTT with Home Assistant auto-discovery: a hand-built binary protocol layer with unit tests, island-mode control, and detection for the moment the battery is full while sun goes to waste.

firmware & home automation

My smart plugs run my own Python firmware — MQTT, relay control, a small web UI. Solar data collection, local LLM and RAG experiments. Same engineering as the day job, smaller blast radius.

learning by reimplementation

To learn a stack properly I rebuild something real: our user-management service exists in my versions in Go (complete — JWT refresh auth, roles, migrations, documented API) and, currently in progress, Spring Boot.

Contact

Looking for platform or backend work, remote-first.