My Work
17 projects, each written up as what I owned, what constrained it, what I traded away, and what changed.
- 001
One tenant model serving a solo practice and a medical group; only Site collapses Solo practice · one tenant Tenant · the practice Site — hidden, collapses to one 2 clinics 2 departments Medical group · one tenant Tenant · the group N sites — visible N clinics N departments Between tenants · unrelated businesses, patient data row-level security enforced in the database no tenant set → sentinel matches nothing, returns zero rows The correction that shaped it A solo dentist with two offices and a general/orthodontic split is an ordinary practice, not an enterprise one — so only Site hides. Clinic and department stay first-class for everyone, which is why every caller states its clinic scope regardless of customer size. Independent A Multi-Tenant Practice SaaS for Solo Practitioners and Medical TeamsA practice-management SaaS I build, deploy and run myself on a VPS. Every customer is a tenant, and one organisational model has to fit both a one-person practice and a medical group — without making the small customer pay for the large one's structure.
- 002
Hibernate 7 switches and restores the schema itself; the provider only registers Per request Resolver fail closed Hibernate 7 setSchema + restore tenant_acme its own schema public registry provider: empty registration only Provisioning · serialised by an advisory lock insert row create schema migrate activate Why the provider is empty Hibernate 7 sets the schema and restores it before releasing the connection. A manual SET search_path — the Hibernate 6 pattern — would double-switch and defeat that restore. Verified with a pool of one: after a scoped session, the connection reports public. Independent Schema-per-Tenant on Hibernate 7A Spring Boot reference architecture giving each tenant its own Postgres schema — where the connection provider deliberately does nothing, because Hibernate 7 already performs the switch itself.
- 003
Job path: outbox to Kafka to Spark, status back over SSE under one trace id Submit · one trace id end to end BFF holds token Outbox same txn Kafka job queue Spark streaming handler per domain status topic → SSE → browser Stuck jobs Reconciler sends bare ids Backend decides re-reads, applies own rules Why the reconciler carries no verdict It sends job ids and nothing else — no thresholds, no reasons. A bug in its query cannot talk the backend into failing a healthy job; it costs a wasted round trip. Independent A Job Execution Platform on Spark and KafkaJobs submitted through a BFF, queued on Kafka, executed by a long-running Spark application and streamed back over SSE under one trace id — with the domain-specific surface narrowed to four extension points.
- 004
Layered ignore rules decide what is published; innermost wins Ignore layers · innermost wins defaults built in .gitignore the vault's own .pkmignore can rescue files nested scoped to subtree Index, rebuilt on every change notes: full text searchable code: names and paths only navigable, not searchable The pruning rule, reproduced on purpose In git, "!dist/keep.ts" does nothing while dist/ is excluded — the walk never opens dist/ to find the exception. Reproducing that means reproducing a limitation, and that is the right call: a tool almost compatible with a familiar format is worse than one exactly compatible, because the gap only ever surfaces as surprise — in the mechanism that decides what becomes public. Independent A Read-Only Reader for Notes and CodeA read-only reader for a notes vault or a code repository — wikilinks, backlinks, search, and a faithful reimplementation of git's layered ignore semantics, pruning behaviour included.
- 005
Tokens layer primitive to semantic to component; components layer primitives to composites to integrations Tokens · three tiers Primitive raw values Semantic what it means Component one component's own components read here Catalogue · the same layering Primitives Button, Chip, Input Composites Card, Stat Badge Integrations wizard, form, stepper Why the third tier exists A kit that only ships primitives passes the burden of composition to every consumer. The integration stories are where a library actually breaks. Independent A Component Library as an Angular WorkspaceAn independently built Material-style UI kit — 35 components, 265 stories — where each component is its own Angular library and every style resolves through a three-tier token system rather than a hardcoded value.
- 006
Where generation is applied, and where decisions stay human Human ownership · cost arrives late architecture domain modelling production-critical decisions reviewable only if the reasoning was made explicit Generated · wrong is cheap and visible Scaffolding boilerplate Tests harness + cases Docs generated, current Prototypes explore, discard judged on quality, maintainability and correctness — not on output volume Why the line sits there Generated code is reviewable. A generated decision is not, because the reasoning that would let you review it was never written down. Independent AI-Assisted Development ToolingCode generation, test scaffolding and documentation generators driven by Claude Code with custom agents and MCP integrations — judged on the quality of what they produce, not the volume.
- 007
Nx dependency graph: the five DataChain modules over shared feature and UI libraries DataChain modules · independently deployable DC Viz DC MarketPlace DC Code DC Tag IA access console Feature libraries feature · lineage feature · transform feature · identity feature · catalog Shared ui · design system data-access · api util · types, testing What the graph buys Dependencies only ever point downward, and Nx fails the build if that is violated. Affected-based CI means touching one feature rebuilds one app, not five. Adobis Nx Micro-Frontend ArchitectureRestructured the front end into an Nx monorepo of five applications on Ports and Adapters, so micro-frontends build and release independently.
- 008
DC Core transformations: Lezer grammar to CodeMirror feedback, then Spark execution In the editor · milliseconds Analyst writes expr Lezer incremental Semantic schema + type CodeMirror errors + completion On submit · cluster Compile to plan dataframe ops Spark executes The trade-off The grammar is maintained twice — once for editor feedback, once for the execution plan. That buys an error in the editor, not a failed cluster job. Adobis DC Core — IDE-Style Formula EditorReplaced raw text files as the way business users author domain rules: a from-scratch Lezer grammar, CodeMirror, and domain-aware autocomplete over their own schema.
- 009
Design system: 15 atomic components under 8 integration components, consumed by every module Consumers module · admin module · portal module · studio module · reports 8 integration components · product opinion filterable table · form layout · wizard · data panel … 15 atomic components · no domain knowledge button · input · select · checkbox · badge · tooltip … Why the split, and what it costs Atomic components stay stable because they know nothing about the domain. Storybook documents each one's states and API — without that a shared library becomes a folder people copy out of rather than depend on. Adobis DataChain Design SystemEnded the duplicated components each team was maintaining — 23 shared Angular Material components, plus the contribution guidelines and versioning that made adoption stick.
- 010
Hexagonal architecture: adapters depend on ports, ports depend on the domain core Inbound adapters REST controller Scheduler MQ consumer Inbound ports Use case interfaces Domain core entities aggregates Outbound ports Repository interfaces Adapters JPA Kafka HTTP Every arrow points inward The core imports nothing from the outer rings, so it runs in a unit test with no database, no broker and no HTTP server. Swapping JPA for another store changes one adapter and leaves the domain untouched. Adobis A Hexagonal Reference Architecture for New ServicesMoved new services off an inherited Java monolith onto DDD and Hexagonal boundaries — writing the reference implementation and driving adoption through pairing sessions.
- 011
DC Maestro: a pipeline as a task graph, validated before save DC Maestro · the process owner edits the graph directly Trigger on event Task A configured Task B configured Decision condition Persist datablock unreachable flagged Validate before save What moves, and what it costs Each node carries its own configuration, so a step is tuned without changing the graph's shape. Moving definitions out of code gives up compile-time checking, so the graph is validated before save instead. Adobis DC Maestro — Visual Workflow EngineThe module where automation processes are composed visually and configured per task, replacing hand-coded process definitions.
- 012
Onboarding programme: a defined path from setup to supervised delivery Before · ad hoc whoever is free answers the same questions again · ramp-up varies by who helps After · defined path · 4 engineers to date 1 · Environment build, run, debug 2 · Architecture why hexagonal 3 · Conventions why the graph rules 4 · Real tickets supervised reasoning taught, not just rules — a joiner who understands why stops asking what The maintenance cost A written programme goes stale faster than code, because nothing fails when it does. It is kept alive by being used every time someone joins, rather than by being reviewed on a schedule. Adobis A Training Programme for New JoinersOnboarding by pairing people through the architecture rather than handing them documentation. Four engineers to date.
- 013
One Keycloak flow for the whole suite, with a custom mapper injecting tenant claims Browser a module UI Keycloak auth server Resource API any module API Custom token mapper adds tenant claim 1 · authorisation code 2 · on issue 3 · JWT + tenant 4 · scoped response Why the mapper exists Tenant identity is not part of the standard claim set. Injecting it at issue time means the API authorises from the token alone — no extra lookup per request, and one flow serves all five tenants. Adobis Keycloak Across Five TenantsKeycloak as the authentication server for the suite — full OAuth2/OIDC lifecycle for hundreds of users across five tenants, with a custom token mapper carrying tenancy.
- 014
Incremental AngularJS to Angular migration with both frameworks running side by side Month 0 7 legacy modules AngularJS 1.x end of life 2021 Months 1 – 18 · both live migrated modules bridge layer remaining legacy Month 18 all 7 on Angular bridge removed dependency gone Why incremental rather than a rewrite A big-bang rewrite would have frozen feature delivery for the whole period. Running both frameworks in one page costs a bridge layer and a heavier bundle while the migration is in flight — a deliberate trade for continuous shipping. The bridge was deleted once the last module moved. Adobis AngularJS to Angular MigrationSeven modules off end-of-life AngularJS over 18 months, both frameworks live behind a bridge so feature delivery never paused.
- 015
Streaming readings into one normalised NgRx store, with views selecting from it UPS estate · continuous readings device · rack A device · rack B device · site NgRx store normalised, one truth RxJS derives per view Views estate overview rack detail alert list components hold no state Why one store, and when it is not worth it Two views showing the same device read the same value by construction rather than by coincidence — views that disagree are the worst failure mode for monitoring software, because nothing signals that it happened. The ceremony is not repaid on a form-driven screen; it is repaid here. Capgemini EATON — Intelligent Power ManagerReal-time energy dashboards for UPS-backed data centres, with facility- and device-level drill-down kept consistent under continuous streams by NgRx.
- 016
BLE device lifecycle: scan, pair, control, with drop-out and recovery paths Happy path Scan discover nearby Pair bond, may fail Control lights, shutters Scenes grouped Reality device drifts out of range · command times out UI shows probable state, retries, never traps the user The design constraint An operation that succeeded a moment ago may not now, so the interface cannot pretend to know a device's state — it represents what is probable and recovers without a restart. Residential software is used by people who did not choose it and will not read documentation, so defaults must be right unconfigured. Capgemini Schneider Electric — Wiser EnergyEnergy and photovoltaic monitoring across EMEA, plus BLE control of lights and shutters from a web app on Android and iOS — where a device is only ever probably in a state.
- 017
SPARK architecture: Java services and a C# projection subsystem bridged by ActiveMQ JVM Spring MVC / Data Apache Storm ElasticSearch ActiveMQ message bridge .NET C# projection calibration physical model AngularJS + ThreeJS Why a broker rather than direct calls The two runtimes have different failure and restart characteristics. A broker between them means the projection side can drop out and recover without stalling the web stack, at the cost of one more piece of infrastructure to run. VISEO SPARK — Spatial Augmented RealityAn EU research project projecting 3D designs onto printed models in real time, with ActiveMQ holding a Java web stack and a C# projection loop apart.