About me
20+ years shipping software · UK
I'm Tomi Doherty — a Wiltshire / Somerset -based software engineer with over 20 years of experience building web and mobile products. My background spans the full stack, but right now my focus is on AI-integrated applications: products where the AI isn't a gimmick, but the thing that makes it genuinely useful.
I've shipped two live AI products independently from the ground up. Before going independent, I spent 8 years as a Software Engineer at Hargreaves Lansdown, building customer portals in PHP, React, Node.js backends, automated report generation systems, and modernising legacy API infrastructure. Before that, roles at T-Mobile, agency work in Bristol,
My broader stack covers PHP / Laravel, React, TypeScript, Node.js, React Native, Python, and Linux DevOps on Docker. I hold a BSc (Hons) in Software Development from the University of Bolton, and I'm currently open to senior full-stack and AI-focused roles in the UK as well as freelance and contract engagements.
Live project
MixGenius
AI cocktail recommendation app. Tell it your mood or a flavour — it generates personalised recipes using a Python AI layer on top of a PHP / Laravel backend, with a React Native app on iOS and Android.
Live project
Screenchat
Real-time movie chat platform with AI film critic personas powered by the Anthropic Claude API. Uses a RAG pipeline, Socket.IO, Docker, MariaDB, Redis, and a React Native frontend — built end to end.
Screenchat, my real-time movie chat platform, has a React-based website frontend alongside its Node.js/MariaDB backend. The website relied on REST-style API calls that returned fixed, often bloated payloads — including nested data like room details, message history, and movie metadata the frontend frequently didn't need in full — increasing payload size and slowing page load and interaction times.
I needed to let the web frontend request exactly the data it needed for a given view, cutting down on over-fetching without maintaining a growing sprawl of bespoke REST endpoints for every use case.
I introduced GraphQL across both sides of the web stack:
API responses shrank significantly by eliminating unused fields and redundant nested data, reducing payload size and improving page load times on the website. The change also reduced backend maintenance overhead, since new frontend data needs could be met by adjusting queries rather than building new REST endpoints.
In Screenchat, a real-time movie chat app I built solo, the chat interface used FlashList backed by a Zustand store to render live, high-frequency message streams across multiple rooms. Users began experiencing chat messages appearing out of order, duplicating, or failing to update in real time — undermining the core “real-time” experience the product promised.
I needed to diagnose why the list wasn't reflecting the true, current state of incoming Socket.IO messages and fix it without degrading scroll performance in long-running chat sessions.
I traced the issue to stale closures — callbacks and render functions inside the FlashList item renderer were capturing outdated references to the Zustand store state from the render at which they were created, rather than the latest state when a new Socket.IO event fired. I:
Chat messages now render reliably in the correct order with no duplicates, even under high message throughput across multiple concurrent rooms — restoring the real-time reliability that's central to the product's value proposition, without any regression in list scroll performance.
MixGenius, my AI cocktail recommendation app with a Laravel/PHP backend and Python AI layer, relies on third-party APIs to power its recommendation engine. As usage grew, uncontrolled API calls from both the website and mobile app risked hitting rate limits, driving up costs, and degrading response times for users.
I needed to protect the third-party API budget and keep the service responsive, without restricting legitimate usage from real users across two different client types — a public website and a mobile app.
I built a caching and throttling layer directly into the PHP API:
The system now controls third-party API spend and protects against abuse or runaway usage, while cached responses improve perceived performance for repeat queries. The dual throttling approach meant genuine users on either platform were rarely, if ever, rate-limited, while automated or excessive usage was reliably capped.
Screenchat's Node.js/MariaDB stack was deployed to a CentOS VPS, but releases were handled manually — pulling code, restarting services, and managing Docker images by hand. This made deployments slow, error-prone, and risky to run frequently, which discouraged shipping small fixes and improvements quickly.
As the sole developer on the product, I needed a repeatable, automated deployment pipeline that could take a code change from commit to live production safely, without requiring manual SSH steps each time.
I built a CI/CD pipeline using GitHub Actions:
Deployments became fast, consistent, and low-risk, removing manual steps that previously slowed down releases. This let me ship fixes and features more frequently and confidently as a solo developer, reducing the operational overhead of maintaining a production service.
Screenchat's movie database suffered from incomplete and inconsistent metadata — missing watch-provider data, no reliable way to track when a title was actually added versus just refreshed, and no clean mechanism to answer real-time questions like “what's new on Netflix” inside the chat rooms.
I set out to build a standalone Python microservice with genuine agentic behaviour — not just a scheduled scraper, but a system that could extract, resolve, and reconcile data autonomously, then let the product's LLM layer query and narrate it safely without hallucinating titles or dates.
I designed and built the pipeline using LangGraph to orchestrate a multi-step extraction agent:
I also wired intent detection into Screenchat's existing socket-based AI-mention handler, so a query like “what's new on Netflix” triggers a direct, cheap database lookup through the service before falling through to general LLM handling, avoiding unnecessary model calls for structured questions.
The microservice closed a persistent data-integrity gap — the database now distinguishes genuinely new titles from routine refreshes, and users can ask natural-language questions about availability and get accurate, non-hallucinated answers grounded in real data. The architecture also gave me a reusable pattern (agent resolves and persists, LLM only narrates) that I've since applied elsewhere in the platform to keep AI features fast and trustworthy.