Skip to main content

Case Studies

Delivered at enterprise scale.

Real delivery work — architecture decisions, trade-offs considered, and measurable outcomes. All client details are anonymised; the technical substance is unchanged.

Featured case studies

Swedish state-owned energy company · Digital Sales · Solution Designer & Senior Developer

Enterprise CMS Migration: EPiServer 11 → Optimizely CMS 12

Led the migration of a large enterprise CMS platform powering digital sales — MyPages customer portal, contract signing flow, and public open pages. The migration moved from EPiServer CMS 11 on .NET Framework to Optimizely CMS 12 on .NET 8+, delivering a cloud-ready, maintainable architecture. A phased side-by-side approach kept end users unaffected throughout the transition.

Key decisions

  • Feature Folder StructureCodebase reorganised by business domain (MyPages, ContractSigning, OpenPages) rather than technical layer — each folder is fully self-contained with its controllers, views, models, and blocks.
  • Side-by-side migration, phased by riskCMS 11 remained live throughout. Open Pages migrated first (lowest business risk), then Contract Signing, then MyPages. One domain at a time — no big-bang cutover.
  • Content Delivery API + Content GraphReplaced legacy property traversal with Optimizely's GraphQL-based content querying — decoupled content delivery, more efficient page composition, no N+1 property fetches.
  • Visual Builder for marketing autonomyEnabled the marketing team to manage page layouts without developer involvement, reducing the volume of content-change tickets routed through engineering.
  • .NET Framework → .NET 6StructureMap IoC replaced with built-in .NET dependency injection — clean container configuration with no third-party IoC overhead alongside the CMS upgrade.
  • AngularJS → Angular ElementsMyPages interactive widgets migrated from AngularJS to Angular Elements (web components). Each Element is independently bootstrapped, lazy-loaded, and self-contained — Angular and the host page are fully decoupled.
Optimizely CMS 12.NET 6C#Feature Folder StructureContent Delivery APIContent GraphAzure DevOpsAngular ElementsDockerCI/CDMulti-sitePersonalization
Architecture decisionsRead more

Why Feature Folder Structure?

The existing codebase was organised by technical layer — all controllers in one directory, all models in another. Understanding any one feature required navigating multiple directories and holding the connections in your head. Ownership was unclear. Reorganising by business feature (MyPages, ContractSigning, OpenPages) made each area self-contained: a developer could understand MyPages by reading one folder. It also clarified team ownership — each product area had an unambiguous home. Onboarding time for new developers dropped measurably.

Phased migration, not big-bang

A full-platform cutover would have required months of parallel work with no user-facing value, and high co-ordination risk at the switch. The phased approach delivered working software at each stage. Open Pages went first — stateless, public-facing, and lowest business risk. Contract Signing followed; its well-defined input/output boundary made it a clean extraction. MyPages, which had the most complex authenticated state, came last. Custom migration scripts handled the content type mapping between CMS 11's property model and CMS 12's content model.

CMS 12 capabilities as force multipliers

Content Graph (GraphQL) replaced slow property traversal on content-heavy pages. Visitor Groups and Personalization enabled context-aware content for authenticated MyPages users without bespoke routing code. Optimizely Forms rebuilt customer-facing forms with validation and backend integration, replacing hand-rolled implementations. Scheduled Jobs automated content synchronisation. These features replaced bespoke code that the team had been maintaining — each one reduced the maintenance surface.

CI/CD pipeline as a delivery prerequisite

The existing deployment process was manual and error-prone. A phased migration over many months requires reliable, repeatable releases — manual deployment was not compatible with that cadence. An Azure DevOps pipeline with automated staging promotion and gated production release was built early in the project. It became the foundation that made incremental migration sustainable.

AngularJS → Angular Elements: migration with best practices

MyPages had interactive widgets (contract status, account details, usage graphs) built in AngularJS. Rather than a full rewrite or a prolonged AngularJS/Angular hybrid period, Angular Elements (web components via @angular/elements) was chosen: each widget compiles to a self-contained custom element, bootstrapped independently, and embedded into Razor views with a single HTML tag. Best practices applied: OnPush change detection to avoid unnecessary rendering cycles; explicit @Input and EventEmitter contracts so the host page has no Angular dependency; lazy loading per element so only widgets present on a given page are downloaded. AngularJS was fully removed from MyPages on the same release — no dual-framework overlap left in the codebase.

Energy sector utility company · Contract Lifecycle Management · Senior Developer & Architect

Electricity Contract Microservice: .NET 8 Web API

Designed and delivered a purpose-built microservice for the full electricity contract lifecycle — new contract creation, existing contract management, and billing data. Built on Vertical Slice Architecture with CQRS and MediatR, the service integrates with upstream frontend systems and downstream SAP/billing backends via Azure Service Bus events. Deployed to Azure Kubernetes Service with Redis caching and zero-downtime rolling updates.

Key decisions

  • Vertical Slice ArchitectureEach feature (CreateContract, UpdateContract, GetBillingDetails) is a fully self-contained slice — command, handler, validator, response model, and endpoint co-located. No shared service layers coupling unrelated features.
  • CQRS with MediatR pipeline behaviorsCommands for writes, queries for reads. Pipeline behaviors apply logging, validation, and error handling automatically — cross-cutting concerns applied once, not duplicated across every handler.
  • EF Core for writes, Dapper for readsEntity Framework Core handles contract write operations with full ORM and migrations. Dapper handles billing read queries with performance-optimised raw SQL for complex joins. Each tool used where it is strongest.
  • Result<T> pattern over exceptionsHandlers return Result<T> instead of throwing for expected business failures. Every failure path is explicit in the method signature, compiler-visible, and independently testable.
  • Azure Service Bus + Polly resilienceContract creation publishes async events consumed by downstream systems. SAP integration via HTTP client with Polly retry and circuit breaker policies. Idempotency keys prevent duplicate contract submissions.
.NET 8C#Web APIVertical Slice ArchitectureCQRSMediatRFluentValidationEntity Framework CoreDapperAzure Service BusRedisDockerAKSAzure DevOpsJWTPollyxUnitMinimal APIs
Architecture decisionsRead more

Why Vertical Slice Architecture over traditional layers?

Traditional horizontal layering (Repository → Service → Controller) couples unrelated features through shared layers — a change to the billing service layer touches code also used by contract creation. Vertical Slice Architecture eliminates this: each slice owns its full implementation. Adding a new feature means adding a new slice; nothing existing is modified, which limits regression risk. On this project, parallel development across team members produced zero merge conflicts on feature work, because each developer owned a distinct, non-overlapping slice.

EF Core and Dapper — why both?

EF Core's change tracking, relationship management, and migration tooling are well-suited to contract write operations where audit trail and transactional integrity matter. For billing read queries involving complex joins across multiple tables, EF Core's generated SQL was verbose and measurably slower than necessary. Dapper's explicit SQL gave full control over query shape and execution plan. Using each tool where it is strongest is more pragmatic than forcing one ORM to cover all access patterns.

Result<T> — why not exceptions?

Exceptions for expected business failures (contract not found, duplicate submission) are semantically wrong and make failure paths invisible at the call site. The Result<T> pattern makes every handler's success and failure cases explicit, compiler-checked, and independently testable. Combined with MediatR pipeline behaviors applying logging and validation automatically, this eliminated the try/catch boilerplate that had been duplicated across every handler in the previous codebase. All failure paths became visible — and tested.

Security designed into the slice contracts

JWT bearer authentication with policy-based authorization is defined per endpoint, not applied as a blanket afterthought. Sensitive contract data is encrypted at field level before persistence. PII is excluded from Serilog structured logs via property filters — the logging contract is part of the slice specification. No PII appeared in any log output. This was specified before any handler was written, not added after go-live.

Swedish state-owned energy company · Frontend Modernization · Senior Developer & Solution Designer

AngularJS → Angular Elements: Yarn Monorepo Migration

Replaced a fragmented AngularJS codebase with a structured Angular Elements monorepo using Yarn Workspaces — producing a library of independently versioned web components deployed to Azure Blob Storage and served globally via Azure Front Door CDN. Components integrate into Optimizely CMS MVC as framework-agnostic custom elements, fully decoupling frontend and CMS release cycles for the first time.

Key decisions

  • Yarn Workspaces monorepo with explicit library boundariesSingle repository with four library tiers: core-lib (services, auth, interceptors), utils-lib (pipes, directives), ui-lib (design system + Storybook), and one feature-lib per business domain. TypeScript path aliases and ESLint import rules enforce no cross-layer dependencies — violations caught at CI, not runtime.
  • Angular Elements as framework-agnostic Web ComponentsEach feature compiles to a standalone Custom Element — self-bootstrapping its own Angular zone, Shadow DOM isolated, exposing HTML attributes and DOM events as its public API. No shared app shell; only elements present on the page are loaded.
  • Immutable versioned deployments to Azure Blob StorageEach build publishes to /elements/v{major}.{minor}.{patch}/feature-name.js. Versioned assets use Cache-Control: immutable with a one-year TTL. A version manifest file maps feature names to current URLs — instant rollback by updating the manifest, no redeployment needed.
  • Azure Front Door CDN with security headers at edgeFront Door sits in front of Blob Storage for global edge distribution. Content-Security-Policy, HSTS, and X-Frame-Options configured at CDN level — applied consistently to every asset, DDoS protection enabled, secondary Blob Storage region as origin failover.
  • Optimizely CMS integration via custom block typesCustom block type per Angular Element — editors place components on pages like any CMS block. Block properties map to element HTML attributes. Version pinning per block enables controlled rollout: v1.x in production while v2.x is tested in staging.
  • Strangler Fig migration — zero downtime, no big bangAngularJS and Angular Elements ran in parallel throughout transition. Feature flags in CMS controlled which version rendered per page. AngularJS fully decommissioned only after all pages verified on Angular Elements.
Angular ElementsYarn WorkspacesWeb ComponentsTailwindCSSStorybookAzure Blob StorageAzure Front DoorCDNOptimizely CMS MVCAngularTypeScriptAzure DevOpsCI/CDVersioned DeploymentsShadow DOM
Architecture decisionsRead more

Why Yarn Workspaces with explicit library boundaries?

A shared repository for multiple Angular libraries eliminates the coordination overhead of separate repos — shared node_modules hoisting, consistent lint and test configuration, single CI pipeline. Yarn Workspaces handles the package linking; TypeScript path aliases and ESLint import rules are what actually enforce boundaries: without them, feature libraries silently depend on each other and the codebase degrades into a coupled monolith. Enforced boundaries made each library's public API explicit — what a library exports is its contract, everything else is internal. The per-workspace build strategy, where each library builds independently, enabled parallel CI steps and isolated deployments per feature. A feature team could ship a new Angular Element without triggering a rebuild of unrelated libraries.

Angular Elements: the self-bootstrapping architecture

The core requirement was embedding interactive Angular components inside an Optimizely CMS MVC application without coupling the frontend build pipeline to the CMS. Angular Elements (Custom Elements / Web Components) solved this cleanly: each element bootstraps its own Angular zone and is embedded via a standard HTML custom element tag — no Angular knowledge required by the host. Shadow DOM encapsulation meant host page styles could not leak into elements and element styles could not leak out. Input configuration via HTML attributes and DOM events as outputs made the CMS integration natural: Optimizely block properties map directly to element attributes, configurable by CMS editors without developer involvement.

Immutable versioned deployments — why and how

The versioned path strategy (/elements/v{semver}/feature-name.js) is the foundation for safe independent deployments. Once a versioned asset is published it is never overwritten — Cache-Control: immutable with a one-year max-age is then correct, and CDN edge caches hold it indefinitely. Rollback is not a deployment: it is a manifest update pointing the CMS block to a previous version path. The version manifest — bypassing CDN cache, always fresh — maps feature names to their current versioned URLs. CMS blocks pin to a major version, so minor and patch releases ship without any CMS change. This was the mechanism that fully decoupled frontend and CMS release cycles.

Strangler Fig: migrating without downtime

A big-bang rewrite of a live customer-facing frontend carries high risk and forces a freeze on feature development. The Strangler Fig pattern avoided both. New Angular Elements replaced AngularJS components incrementally — one page area at a time. Feature flags in Optimizely CMS controlled which version (AngularJS or Angular Element) rendered per page, enabling per-page validation before traffic switched permanently. AngularJS was not decommissioned until every page area was verified on the new stack. The result: a full platform migration with no user-facing downtime and no frozen development period.

TailwindCSS as a shared design token layer

With multiple Angular libraries building independently, visual consistency across feature teams is a genuine risk — each team can drift toward their own spacing, color, and typography conventions. A shared Tailwind preset exported from ui-lib solved this: all feature libraries extend the same base configuration, so design tokens have a single source of truth. Tailwind's per-library content configuration eliminates unused CSS in each build output — the shared layer adds no bundle overhead. The monorepo-level Storybook instance, documenting all ui-lib components with stories for default state, variants, and edge cases, became the shared design system reference for both developers and designers — reducing misalignment in review cycles.

More case studies in progress — CI/CD pipeline design and cloud-native patterns coming next.

Discuss your project →