Backend architecture · Team workflow · July 2026, then iterated
Turning a monolithic BFF into feature-owned slices without changing a contract
During an incremental React migration, the ASP.NET Core backend-for-frontend was the integration point between a multi-tenant web application and its internal API — and its HTTP wiring, orchestration, contracts and test setup were concentrated in a few large shared files that every developer had to touch. I led the refactor into feature-owned endpoints, services, contracts and tests, protected by Swagger snapshots at every checkpoint: 307 operations preserved, zero handlers left in the composition root.
- My role
- Architecture, extraction sequence, composition model, compatibility checks and release safety: mine. The feature logic that moved: its authors'.
- Stack
- ASP.NET Core
- C#
- Endpoint routing
- Dependency injection
- Swagger / OpenAPI
- NUnit
At a glance
- Central endpoint file 5,657 → 81 lines; 128 registrations → 0 handlers
- All 307 Swagger operations preserved at the staging cutover
- Running in testing, staging and production within roughly two weeks
Context
The BFF sits between a multi-tenant web application and its internal API and already exposed hundreds of operations. Its HTTP wiring, application orchestration, contracts and test infrastructure lived in a few large shared files: the main endpoint file had reached 5,657 lines with 128 HTTP registrations (133 verb-path operations across 19 tags), one shared service mixed unrelated domains, and production and test dependency registration could drift apart.
With several developers building new React areas at the same time, everyone needed the same BFF surfaces. The structure had become a merge and coordination bottleneck more than a code-quality problem — and the goal was to make each feature independently understandable and changeable while preserving routes, payloads, status codes, authentication, tenant behaviour and frontend compatibility.
What I did
- Inventoried the HTTP surface and froze it in Swagger/OpenAPI snapshots before moving anything.
- Designed the feature slice: React / RTK Query → /api/bff → feature endpoint module → feature service → user-scoped downstream API client → feature contract → frontend adapter and UI model.
- Introduced one shared composition method for dependency registration, used by both the production host and the test host, so the two could no longer diverge; session validation, tenant context and downstream API access stayed shared instead of being copied into each slice.
- Moved endpoint clusters, application logic and DTOs in controlled checkpoints, comparing route, operation ID, tag and payload metadata with the baseline after each step.
- Added startup, route, operation-ID, tag and dependency-resolution guardrails, plus an explicit build/freshness check after discovering that a stale compiled assembly could make the Swagger comparison pass falsely.
- Wrote Markdown and HTML onboarding material for developers working in the new structure, and later optimized the local quality gate and the shared test fixtures when the first version proved too slow for everyday pull-request work.
Decisions
Extraction, not rewrite
Existing feature logic kept its behaviour and its authors. My responsibility was the boundaries, the sequence and the safety net, not re-implementing business rules.
Contract first
OpenAPI snapshots defined what 'unchanged' meant: routes, payload shapes, status codes, authentication and tenant behaviour had to match the baseline at every checkpoint before the next one started.
One composition root for production and tests
The same registration catalogue feeds both hosts. Cross-cutting concerns stay shared; a slice owns only what is specific to its feature.
Strict gate first, then make it cheap
The first quality gate protected contracts and tests but asked developers to remember extra steps, and some local checks were slow enough to interrupt normal PR work. Keeping the protection while cutting the cost was the second half of the job.
The hard part
The refactor had to run while the same files were being changed by other developers for new features, so it moved in checkpoints small enough to merge and verify — never as one long-lived branch.
The false-green discovery mattered: a stale compiled assembly made the contract comparison pass although the code had changed. Making the guard build and check freshness first turned it from a comfort into a real safeguard.
Result
- At the staging cutover the central endpoint file went from 5,657 to 81 lines and from 128 registrations to zero handlers; dedicated endpoint files grew from 18 to 42, BFF service classes from 20 to 40 and feature contract files from 18 to 40, with all 307 Swagger operations preserved.
- The structure reached testing, staging and production and became the pattern the team uses. I observed no regressions caused by the extraction, and within roughly two weeks of identifying the bottleneck the modular structure was operating across the environments.
- New areas could be built in parallel without concentrating unrelated work in one endpoint, service, contract or test file; request paths became easier to trace, for people and for AI-assisted tooling alike.
- At a later snapshot the architecture had grown to 92 endpoint modules, 91 service classes and 93 contract files, still with no handlers in the composition root — growth that belongs to the whole team's product work, not to the refactor alone.
Numbers, with their limits
- Central endpoint fileBefore5,657 linesAfter81 linesthat file only; logic redistributed, not deleted
- HTTP handlers in the composition rootBefore128 registrationsAfter0
- Dedicated endpoint filesBefore18After42
- BFF service classesBefore20After40
- Feature contract filesBefore18After40
- Swagger operationsBefore307After307preserved at the staging cutover
What these numbers do not say
- The line-count reduction applies to the central composition file: total BFF code and complexity were redistributed into feature boundaries, not cut by the same proportion.
- No independent metrics were kept for merge-conflict frequency, lead time, defect rate or hours saved. The rollout to every environment and the absence of regressions rest on direct engineering observation.
- The later counts (92 / 91 / 93) reflect continued team development, and the speed-up of the quality gate was not benchmarked.
What it proves
I can take a shared bottleneck in a live backend and restructure it under a contract-preservation guarantee, in checkpoints the team can absorb — and then make the safeguards cheap enough to survive daily use.
Attribution
The extraction, the composition model and the guardrails were mine, with AI-assisted analysis and documentation under my review. The features moved into the new slices were written by teammates and remain theirs.