Case studies

The work,
and the reasoning

Not a portfolio. These are walkthroughs of real systems I've built — anonymized to respect client confidence, and written to show how the decisions were made rather than just what got shipped. The interesting part of engineering was never the technology list. It was the constraints, the trade-offs, and the judgment calls in between.

Client names, proprietary metrics, and identifying detail have been deliberately removed. What remains is the engineering — which is the part worth reading anyway.

case-studies / ev-charging-infrastructure

Case Study 01 · EV Charging Infrastructure

Untangling a national charging network

Every team was wiring directly into the same third-party APIs. One change could silently break three others.

Domain

EV Charging Infrastructure

Timeline

12 months

Role

Lead Architect & Engineer

Scale

1 → 7 sales channels

01Overview

A national EV charging operator had outgrown its own architecture. What started as a single product had sprawled into multiple teams shipping against a shared monolith and a tangle of direct third-party integrations. The platform worked — until it didn't, in ways nobody could trace. The mandate was to make the system safe to change again, and to do it without pausing the business.

02The problem

Multiple teams integrated independently against the same external systems — CRM, payments, identity, analytics. There was no single owner of any integration, so a contract change from one provider could break unrelated features with no warning and no clear blame surface. Observability was thin: when something failed in production, tracing the cause meant reading logs across services that didn't share a correlation model. The monolith underneath couldn't absorb the pace the business now demanded.

03Constraints
  • ×Zero downtime — the charging network is live infrastructure people depend on
  • ×Multiple autonomous teams already mid-flight on their own roadmaps
  • ×Existing third-party contracts and rate limits could not be renegotiated
  • ×Migration had to be incremental — no big-bang rewrite was acceptable
04Architecture

The core move was to stop letting every service talk to the outside world directly. I introduced a proxy service layer that owned each third-party relationship, exposed a stable internal contract, and absorbed provider-side churn. Services stopped integrating with vendors and started integrating with us. Asynchronous, event-driven communication via a message bus decoupled the services from each other so a slow or failing downstream couldn't cascade.

system-architecture.svg
Channels

Consumer App

B2B Portal

Partner Sites

Onboarding Flow

Gateway

API Management

Auth · routing · rate limiting

Services

Domain Services

Async via Service Bus

Integration Proxy

Owns every 3rd-party contract

External

CRM

Payments

Identity

Analytics

Fig. 01 — highlighted nodes mark the components I designed and owned

05Technologies

Runtime

ASP.NET / .NETAzure Container Apps

Messaging

Azure Service BusEvent-Driven Architecture

Edge

Azure API Management

Data

MongoDBDistributed Transactions

Ops

Infrastructure as CodeAzure Monitor
06My role

I owned the architecture end to end and wrote a large share of the code myself. That combination mattered: the design decisions were tested against the reality of implementing them, not handed off as a diagram. I also worked across the autonomous teams to migrate them onto the proxy layer one integration at a time, which was as much a coordination problem as a technical one.

07Interesting technical challenges
01

Migrating live integrations without a maintenance window

Each third-party integration had to move from direct-access to proxy-mediated while traffic kept flowing. I used a strangler approach — the proxy shadowed real traffic first, then took over reads, then writes, with the old path kept warm as a fallback until confidence was high. No single switch was ever flipped on the whole system.

02

Designing for partial failure

In a distributed system, the question isn't whether a downstream fails but what happens when it does. I made failure modes explicit: idempotent message handling so retries were safe, dead-letter queues for messages that couldn't be processed, and circuit breakers so a struggling provider degraded one feature instead of taking down the platform.

03

Making the invisible traceable

A request might cross four services before touching an external system. I introduced a shared correlation model so a single identifier followed a request across every hop, turning 'something is slow' into a question that could actually be answered.

08Lessons learned

Centralizing a relationship — not just code — is the real win. The proxy's value wasn't technical elegance; it was giving every third-party integration a single owner.

Incremental migration is slower and almost always correct. The strangler pattern cost more upfront and saved the project from the risk no one could afford.

Observability isn't a feature you add later. If you can't trace a request end to end, you don't really understand your own system.

case-studies / industrial-distribution

Case Study 02 · Industrial Distribution

A B2B commerce platform that thinks in workflows

Thousands of orders a month were arriving by email and phone. The systems behind them couldn't model how the business actually sold.

Domain

Industrial Distribution

Timeline

9 months

Role

Lead Full-stack Developer & Architect

Scale

40+ enterprise accounts onboarded

01Overview

A mid-sized industrial distributor was processing high-volume B2B orders manually — email, phone, spreadsheets. Their enterprise customers expected negotiated pricing, bulk ordering, and multi-step approval flows that off-the-shelf commerce couldn't express. The brief was a self-service platform that matched how the business genuinely operated, not a generic storefront bolted onto a catalog.

02The problem

B2B commerce is not consumer commerce with bigger numbers. Each customer had its own negotiated price list. Orders frequently needed internal approval before they could be placed. Buyers ordered in bulk against contracts, not impulse. The existing fragmented systems couldn't represent any of this, so the work fell to humans re-keying orders — slow, error-prone, and impossible to scale.

03Constraints
  • ×Pricing logic had to stay correct to the cent across thousands of contract permutations
  • ×Existing ERP was the source of truth and could not be replaced
  • ×Customer approval hierarchies varied per account and had to be configurable
  • ×Launch needed to onboard real enterprise accounts within the first quarter
04Architecture

An API-first commerce core handled catalog, cart, and checkout, but the interesting work lived in the layer between it and the ERP. A custom pricing engine resolved the right price for the right customer at request time, and a configurable workflow engine modeled each account's approval hierarchy as data rather than code. The ERP remained the system of record; everything synced back to it.

system-architecture.svg
Frontend

Self-Service Portal

Next.js · TypeScript

Commerce

Commercetools

Catalog · cart · checkout

Pricing Engine

Per-customer resolution

Approval Workflows

Config, not code

Integration

Sync Layer

Bi-directional, idempotent

Systems

SAP ERP

Source of truth

Salesforce

DocuSign

Fig. 02 — highlighted nodes mark the components I designed and owned

05Technologies

Frontend

Next.jsTypeScriptGraphQL

Commerce

CommercetoolsCustom Pricing Engine

Integration

SAP ERPSalesforceDocuSign

Data

SQL ServerAzure
06My role

I led both the architecture and the full-stack build. The defining decisions were in the modeling: choosing to treat pricing and approvals as first-class engines rather than special cases scattered through the codebase. I designed those engines, built the ERP sync layer, and shaped the API contracts the frontend consumed.

07Interesting technical challenges
01

Pricing that had to be right every single time

A wrong price in B2B isn't a UX annoyance — it's a contractual problem. The pricing engine resolved customer, contract, volume tier, and product into a single authoritative price at request time, with the resolution logic isolated and exhaustively tested so it could be reasoned about and trusted.

02

Modeling approval as data, not branches

Every account approved orders differently. Encoding that as conditional logic would have been a maintenance trap. Instead I built a workflow engine where an account's approval hierarchy was configuration — new structures could be expressed without shipping code.

03

Keeping two sources of truth honest

The platform and the ERP both held order state, which is a recipe for drift. The sync layer was idempotent and reconciling: it could be replayed safely, and it actively detected and resolved divergence rather than assuming the two systems would stay aligned on their own.

08Lessons learned

In B2B, the domain model is the product. Get pricing and approvals right and the UI almost designs itself; get them wrong and no interface can rescue it.

Configuration beats code for anything that varies per customer. The workflow engine paid for itself the first time a new account onboarded without a deploy.

Two systems holding the same truth will drift. Plan for reconciliation from day one, not as an incident response.

case-studies / nordic-media-and-publishing

Case Study 03 · Nordic Media & Publishing

Moving a decade of publishing without losing a word

A decade of content, multiple brands, and editorial teams who couldn't afford to stop publishing for a single day.

Domain

Nordic Media & Publishing

Timeline

24 months

Role

Senior Backend Developer

Scale

Multiple brands · millions of readers

01Overview

One of Scandinavia's largest media groups was constrained by an aging CMS that couldn't keep up with their content velocity or multi-brand ambitions. The task was to migrate multiple brand properties to a modern content platform — without losing content, breaking URLs, or slowing the editorial teams who publish continuously.

02The problem

Migrations are where systems go to reveal their hidden assumptions. A decade of content had accumulated quirks: inconsistent structures, undocumented edge cases, content models that had drifted over years. The new platform expected clean, well-formed data. Between those two realities sat the actual work — and it had to happen under live properties that readers and advertisers depended on every hour.

03Constraints
  • ×Zero critical data loss was non-negotiable
  • ×Live properties had to stay up throughout — no publishing freeze
  • ×Existing URLs had to be preserved for SEO and reader trust
  • ×Editorial workflows had to improve, not just survive the move
04Architecture

The migration was treated as a pipeline, not an event: extract legacy content, transform it into the new model, validate it, load it, and verify. Each stage was idempotent and re-runnable so the whole process could be rehearsed repeatedly against production-scale data before anything went live. The new platform integrated with existing cloud services so the surrounding ecosystem didn't have to move at once.

system-architecture.svg
Source

Legacy CMS

~10 years of content

Pipeline

Extract

Transform

Map to new model

Validate

Reject before load

Load

Target

Optimizely CMS

Multi-brand

Optimizely Commerce

Platform

Azure Services

Fig. 03 — highlighted nodes mark the components I designed and owned

05Technologies

Platform

Optimizely CMSOptimizely Commerce

Runtime

ASP.NET.NET Core

Cloud

AzureSQL Server
06My role

As a senior backend developer on the migration, I focused on the transform-and-validate core — the logic that mapped messy legacy content into the new model and refused to load anything that didn't meet the target schema. I worked closely with editorial teams to understand which 'quirks' were actually meaningful and which were genuinely safe to normalize away.

07Interesting technical challenges
01

Validating before loading, every time

The fastest way to corrupt a new system is to pour bad data into it. The pipeline validated aggressively at the transform stage and refused to load anything malformed, surfacing the problem as a fixable report instead of a silent inconsistency discovered months later.

02

Rehearsing against production scale

A migration that only runs once, in anger, is a gamble. Because every stage was idempotent, the full pipeline could be run repeatedly against real-scale data. By go-live, the migration had been rehearsed enough times that the live run was the least surprising part of the project.

03

Distinguishing quirk from intent

Not every inconsistency in a decade-old system is an accident. Some encode real editorial meaning. The judgment work — done with the people who actually used the system — was deciding what to preserve faithfully and what to clean up, and that judgment mattered more than any single line of code.

08Lessons learned

A migration is a pipeline, not an event. Make every stage idempotent and you buy the right to rehearse until you're confident.

Validate at the boundary, loudly. Bad data caught at transform time is a report; bad data caught after load is an incident.

The hardest part of legacy work is human, not technical. Knowing what a quirk means requires the people who lived with it.

There's more I can't write about here.

Client confidentiality keeps a fair amount off the page. If you're curious about the parts I can't publish — or thinking through a similar systems problem — I'm glad to talk it through.