smallbox

← All work

Case study

Legacy MVC replacement: gradual extraction or controlled twin?

A detailed reference document on a single modernization decision, written as the input the companion sample System Report is reasoned from.

The company described here — NaboWorks A/S — is fictional. The business model, the team shape, the operational mess, and the modernization options are deliberately built as a coherent composite of real ASP.NET MVC service marketplaces, so the decision can be reasoned through against something concrete instead of generic audit language. There is no real client behind this page. The pattern, the framing, and the decision discipline are not fictional; they come from operating an ASP.NET MVC service marketplace through its entire life. Where the page leans on real experience, it says so.

What this case demonstrates

  • How Smallbox decides between gradual extraction, controlled twin replacement, and stabilise-first. Three serious outcomes — none of them assumed in advance, all three named so the report can choose on evidence rather than on a vendor's preference.
  • Why "new frontend" is really a business-rule and change-safety problem. In a mature MVC system, the rules live in controllers, services, stored procedures, SQL views, and admin overrides — not in the views. Any modernization that ignores that is a quiet rewrite of business logic by a team that does not yet know the rules.
  • Why the parity map matters. Before code is moved, "the same system" has to be defined: what must be preserved exactly, what should be preserved in behaviour but improved in structure, what should be deliberately changed, what should not be copied, and what is still unknown. The parity map is the contract every later recommendation answers to.

Map


NaboWorks A/S

NaboWorks is a 12-year-old Danish local-services marketplace operating in Copenhagen, Aarhus, Odense, and Aalborg. It connects households, landlords, housing associations, and small office managers with vetted local providers for move-out cleaning, handyman visits, painting, gardening, minor repairs, and light maintenance. The business deliberately combines fixed-price and negotiated work: some categories are structured enough to estimate quickly; others require clarifying questions or an assessment visit before a credible offer can be made. Real service marketplaces often live somewhere between instant booking and negotiated fulfilment rather than at one clean extreme, and NaboWorks reflects that.

The company makes money in two ways. The primary engine is a commission on completed jobs where the customer pays through the platform and the provider is paid out later. A secondary stream comes from provider subscriptions that unlock faster lead access in certain categories and service areas. Keeping the transaction on-platform is structurally important — every interaction the platform fails to host is one the parties can complete privately and pay for elsewhere. Booking-flow design and payment design are tightly linked.

NaboWorks has 58 employees:

  • ~16 in customer support and operations
  • ~8 in provider success and trust
  • ~4 in finance
  • ~4 in product and engineering
  • the rest in growth, partnerships, marketing, and management

This is not a software company with a side application. It is a service business whose revenue, customer experience, and internal coordination all depend on one ageing system. Support staff live inside it minute by minute. Finance depends on it weekly. Management watches its reports daily. Sales and provider onboarding can keep using other tools if they must, but the marketplace itself is the operational centre of gravity.

What an outage actually means

DurationEffect
1 hourNew requests back up. Support loses live visibility into bookings and provider contact state. Some reminders or provider alerts may fail silently.
1 dayThe office can still answer the phone and capture rough details in spreadsheets, but cannot safely run the normal request → offer → booking flow, cannot trust notification timing, cannot run payouts with confidence.
1 weekReputational hit with both customers and providers. Support moves to shadow operations in email and spreadsheets. Finance moves from managed exceptions to outright manual survival.

Other tools, none of them replacements

Support uses a helpdesk. Finance uses an accounting package and imports payout/export files. Marketing runs email campaigns elsewhere. Management has a dashboard layer for aggregated reporting. Provider success uses a CRM for sales-style follow-up. None of those tools is the system of record for a request, an offer, a booking, a provider payout, a refund case, or an admin override. That system of record is the old MVC application.

How the company got here

NaboWorks was built between 2013 and 2016 by a local .NET consultancy. The first system solved the company's most important early problem: capturing demand and letting local providers respond. Over twelve years the company layered on categories, geographies, notification rules, payment logic, trust controls, admin tools, finance exports, provider documents, and reporting.

The trigger for reaching out to Smallbox is business pressure, not architectural ambition. Mobile conversion on the public request flow is disappointing. Growth wants faster changes to landing and intake flows. Provider onboarding changes are slower than the commercial team can tolerate. Customer support is buried in avoidable edge cases and duplicate handling. Management thinks it wants "a modern frontend and cleaner API". What it is actually buying from Smallbox is a decision: should NaboWorks gradually extract the old MVC application, or build a clean replacement beside it — and what is the first safe package either way?


Team and ownership

The original consultants who built the first three years are gone.

Inside NaboWorks today:

  • Engineering lead. One person, ten years inside the system. Knows most of the code but not everything. Has accumulated caution about specific modules. Personally signs off most production deploys.
  • Two developers. One has been with NaboWorks for three years and is comfortable across the codebase but defers to the lead on payment and finance areas. One joined a year ago and is still mapping the estate.
  • A QA / ops hybrid. Owns the deployment routine, the staging environment as it actually exists, the snapshot process, and the unwritten knowledge about which jobs need to be paused locally. Single point of operational knowledge.
  • A long-serving operations manager. Outside engineering. Knows more about real booking and payout edge cases than any developer. Where the engineering team has a question about what is supposed to happen, this is who they ask.

The knowledge map is asymmetric. The lead knows the code; the ops manager knows the business; one developer is still ramping; the QA hybrid is the only person who can deploy confidently. A vendor relationship that survives only by personal knowledge of one or two people is a vendor relationship that breaks the day one of them leaves — and a System Report names that explicitly even when it is not the client's primary outcome.

For Smallbox, the practical implication is that all serious System Report interviews involve the operations manager, not just the engineering lead. The technical owner answers how things are built; the ops owner answers what is supposed to happen, and the gap between those two answers is often the most useful evidence in the report.


A day inside NaboWorks

The system is best understood through how the business uses it, not through its file layout. This section walks every major actor: the customer, the provider, the support team, the trust team, finance, management, and the background jobs.

Customers

Customers arrive through search, a category landing page, a repeat-client link, or a property-manager portal. They pick a service category, enter an address, date window, contact details, optional photos, free-text notes, and sometimes an expected budget.

For move-out cleaning or simple window cleaning, the form is structured and NaboWorks can show a price band or gather enough information for providers to quote quickly. For painting, repairs, or handyman visits, the public flow still captures structured information, but the real commercial process is request-first: providers or operations need to ask follow-up questions, or an assessment visit may be booked before a final offer is trusted.

Once submitted, the customer receives an acknowledgement and the request enters an open marketplace workflow. In some cases the customer can edit or cancel the request before a provider has been chosen. In others, support intervenes first because the request is ambiguous, urgent, out of area, or likely to create duplicate work.

Customer-visible states are intentionally simple:

request received → providers reviewing → offers available → booked → scheduled → completed → under review → refunded

Underneath that simplicity, the internal workflow is more granular, gated, and admin-touched than the customer ever sees.

When offers arrive, the customer compares price, time window, provider rating, completed-job count, and written notes. The customer may choose directly, or support may recommend a provider in higher-friction categories. After the work is marked complete, the customer receives an invoice, download access, and a review request. For repeat clients, a lightweight self-service area shows upcoming work, lets them download documents, approve certain actions, and request further work in one place.

Providers

Providers do not just create a profile and start earning. NaboWorks requires payout details, identity or business verification, category qualification details, insurance or compliance documents for some categories, and service-area configuration. Providers can choose the categories they serve, the postcodes or radius they cover, the times they are unavailable, and certain preferences around lead volume.

Provider onboarding is separate from customer booking. It can impose non-trivial support and compliance obligations, especially when the platform wants strong operational control over who can earn.

After onboarding, providers receive matching requests in waves. NaboWorks does not blast every request to every provider. Matching considers:

  • service category
  • service area
  • current pause state
  • document validity
  • historical reliability
  • rating thresholds
  • a few commercial rules that have accumulated over the years

Some high-value or urgent categories go through a narrower first notification wave before broadening out. Providers can decline, ignore, ask follow-up questions, or submit an offer with price, availability, notes, and in some categories a proposed assessment slot. A chosen provider then sees the customer's details, receives reminders, can message the customer or support, marks work progress, uploads photos or notes for proof in specific categories, and eventually becomes eligible for payout.

A provider's lived experience of "the platform" is not abstract. It is whether the right jobs arrive, whether the timing is fair, whether cancellations are handled sensibly, whether reviews feel honest, and whether payouts land when expected. Payout timing, dispute recovery, reminder timing, and review publication all belong in the parity map later in this page rather than being dismissed as back-office details.

Support and operations

Support and operations are the real day-to-day owners of the system. Their work includes:

  • Handling duplicate requests.
  • Merging customer records.
  • Reclassifying misfiled categories.
  • Correcting addresses.
  • Manually scheduling assessments.
  • Reopening or archiving dead requests.
  • Resending booking confirmations.
  • Handling provider no-shows.
  • Creating goodwill credits.
  • Explaining invoice discrepancies.
  • Guiding customers who are uncomfortable choosing from competing offers.

They also work around the system's weak spots. If the public flow allows a customer to submit something ambiguous, support absorbs the ambiguity. Support's job is partly to be the system's elastic boundary against the messiness of real life. That is true everywhere; what matters here is that any modernization plan that breaks support's existing workarounds without replacing them is a plan that quietly costs the business more than the engineering it saved.

Provider success and trust

Provider success and trust sit beside support but focus on a different layer:

  • onboarding completion
  • provider fraud signals
  • document expiry
  • review disputes
  • complaint handling
  • temporary suspensions
  • provider quality patterns

A provider with weak ratings, missing documents, or repeated disputes may still exist in the system but be prevented from seeing new demand, prevented from receiving payouts, or quietly routed out of specific categories. The presence of three or four overlapping mechanisms for "this provider should not see new demand right now" is exactly the kind of distributed business rule that makes a frontend-only rewrite dangerous.

Finance

Finance works in a thinner but more consequential slice:

  • captures
  • failed payments
  • provider statements
  • payout batches
  • refund cases
  • dispute recovery
  • VAT handling
  • accounting exports

In a platform-style payment model — separate-charges-and-transfers, in the language of the underlying integration — the platform balance bears fees, refunds, and chargebacks, and refunds do not automatically unwind downstream provider transfers. Someone has to reverse, withhold, or reconcile the money. At NaboWorks, that someone is a combination of code, scheduled jobs, finance screens, and a few manual interventions.

The finance team's lived requirement is that trusted totals stay trusted. They can defend a process whose outputs they trust even if the process is ugly. They cannot defend a clean new process whose outputs disagree with the old one. This is the hidden budget constraint on any modernization: parity of outputs is more valuable than parity of implementation.

Management and reporting

Management uses the system more indirectly but materially. They watch:

  • request volume by category and city
  • provider response speed
  • request-to-booking conversion
  • cancellation rates
  • payout backlog
  • invoice ageing
  • support workload
  • the performance of partner segments such as property managers

These reports are often generated by a mix of EF queries, raw SQL views, and downstream export pipelines. Some of them have been "correct" for years in the sense that the same number lands in the same place reliably, but the actual definition behind that number may have drifted from what management thinks it represents. A finding the System Report frequently surfaces in mature systems is that "trusted metrics" are trusted because they are stable, not because they are defined.

Background jobs and recurring pain

Much of NaboWorks' business value sits in background work that end users barely notice unless it fails:

  • Initial provider alerts.
  • Broader second-wave notifications.
  • Booking confirmations.
  • Appointment reminders.
  • Invoice follow-ups.
  • Review prompts.
  • Payout batches.
  • Export files.

Another set closes stale requests, flags inactivity, or moves bookings into "action required" states that tell staff something has stopped moving and now needs human intervention. Status names like pending, unscheduled, upcoming, action required, archived, converted are not cosmetic. They are part of how human teams know where work is stuck.

The support-ticket pattern at NaboWorks is predictable:

  • Customers submit duplicates because they are unsure the first form worked.
  • Providers open a job and discover the scope is materially different from the intake.
  • A booking confirmation goes out, but the payment state is still unresolved.
  • A provider marks work complete, but the customer disputes the outcome.
  • A partial refund leaves finance with a provider balance problem.
  • An admin reopens a request to help a customer, and an old reminder rule sends an extra message that confuses everyone.

These small, operationally expensive edge cases are exactly what later determines whether a gradual extraction is genuinely safe or only feels safe from an architecture diagram.


The technical estate

Stack and repository shape

NaboWorks runs on ASP.NET MVC 5 on .NET Framework 4.7.2 with Razor views, jQuery, Bootstrap 3, and a small amount of hand-written vanilla JavaScript. Persistence uses Entity Framework 6 in a database-first style with an older EDMX model for core entities, a set of hand-written service classes, and several stored procedures and SQL views for finance-heavy reporting. The live database is SQL Server.

External integrations:

  • email (transactional mail provider)
  • SMS (gateway)
  • maps and postcode normalisation (geocoding service)
  • payments / payouts (Stripe-Connect-style marketplace integration)

Background work is split awkwardly between Hangfire and a couple of older scheduled executables that nobody has properly retired.

The source tree is messy but recognisable:

/src/NaboWorks.Web
  /Areas/Admin
  /Areas/Provider
  /Controllers
  /Views
  /Scripts
  /App_Start
/src/NaboWorks.Services
/src/NaboWorks.Data
/src/NaboWorks.Integrations
/src/NaboWorks.Jobs
/tests/NaboWorks.Tests
/sql

The web project carries public pages, provider pages, and most admin screens in one deployable unit. The services library is real, but not authoritative: some business logic lives there, some in controllers, some in SQL, some in HTML helpers, and some in jobs. The data project exposes EF-generated entities directly enough that Razor views frequently depend on them. The integrations project wraps payment, email, SMS, and accounting concerns, but often too thinly to protect the rest of the code. The jobs project reuses business services in some places and bypasses them in others.

Data model and coupling

The core entities are unsurprising on paper:

Customer, Provider, ServiceRequest, RequestPhoto, Offer, Booking, MessageThread, Invoice, Payment, ProviderPayout, AdminAction, Notification, RequestStatusHistory, ProviderCategory, ServiceArea, ComplianceDocument, Review, AuditLog.

The problem is not the nouns. The problem is the number of places where a single business rule can act on them.

A request status may be implemented as an integer enum in C#, a string in an export, a CSS branch in a Razor view, and a filter condition in a finance SQL view. Provider eligibility may depend partly on a Provider.IsActive flag, partly on whether documents are current, partly on a quality or complaints table, and partly on whether a specific admin note has toggled a hidden hold state. Invoice totals are trusted by finance, but the path to those totals may cross controller-level adjustments, stored-procedure aggregation, and downstream export formatting. None of this is catastrophic by itself. Taken together, it means the application contains behaviour in more places than the team can name confidently.

Representative trace paths

Five flows a System Report would walk end-to-end, with their failure modes named explicitly. These are the flows the engineering work would have to protect first, and they are the trace paths a sample report would document in its appendix.

1. Public customer request creation. Begins in RequestController.Create and RequestController.Submit. The controller validates category, address, lead window, and attachments, calls a service that creates or updates a draft, geocodes the location, writes a ServiceRequest, stores RequestPhoto rows, derives a service-area match, writes an initial RequestStatusHistory event, and enqueues outbound notifications.

  • Tables touched: Customer, Address, ServiceRequest, RequestPhoto, ServiceArea, Notification, AuditLog.
  • External services touched: geocoding, email, SMS.
  • Failure modes: duplicate creation on repeated submission; notification side effects firing before transaction completion; postcode mismatches that put the request into a manual queue; attachments uploaded but their references missing if the parent transaction fails.
  • What test would protect it: a Facade-level integration test that drives the controller through a real database and a stub-but-honest notification surface, asserting (a) one row created per logical submission, (b) no notification fired before commit, (c) the status-history event matches the persisted state.

2. Provider offer submission. A provider-area controller action loads the request, validates eligibility, looks up service-area and category compatibility, checks compliance holds, creates an Offer, appends a status event, and sometimes opens a message thread.

  • Tables touched: Provider, ServiceRequest, Offer, ProviderCategory, ComplianceDocument, Notification, RequestStatusHistory.
  • External services touched: email, SMS.
  • Failure modes: race conditions where a provider submits an offer just after the request has closed; locale-specific pricing validation bugs (decimal separator, VAT inclusive vs exclusive); hidden provider-tier rules that change who may see a request in the first place.
  • What test would protect it: a characterisation test that captures the matching decision for a known set of (request, provider) pairs and freezes that as the spec, plus a concurrency test for the close race.

3. Admin override / support intervention. Support staff can reopen requests, reassign categories, resend confirmations, apply credits, archive records, or directly change status on a booking or request. These actions usually write AdminAction and AuditLog rows, but they can also bypass the same validations that the public and provider flows use.

  • Tables touched: almost everything; specifically ServiceRequest, Booking, Invoice, Payment, ProviderPayout, AdminAction, AuditLog.
  • External services touched: email, SMS, occasionally payment.
  • Failure modes: bypassed validation leaving an entity in a state the public/provider flows cannot reach but the system has to handle later; re-firing of obsolete reminders after a status reopen; manual credits that are not reflected in the trusted finance reports.
  • What test would protect it: approval tests on the admin actions most likely to leak, captured against a fixture, plus an explicit invariant that no admin action may persist a state the public flow would reject.

This is the most dangerous trace in the estate because it contains operational authority, not just convenience. When the company says "the admin area works", what it often means is "support knows which breaks it can safely touch."

4. Invoice and payout. Once work is completed, an invoicing service calculates amount, commission, VAT, payout amount, and capture rules. It writes an Invoice, a Payment or payment-intent state, and eventually a ProviderPayout candidate. A payout batch job later groups eligible lines, exports or calls payment APIs, and marks the payout state.

  • Tables touched: Booking, Invoice, Payment, ProviderPayout, Review, AccountingExport, ledger-like tables for negative balances and withheld amounts.
  • External services touched: payment / payout, accounting export.
  • Failure modes: partial refunds after payout; repeated webhook delivery; stale transfer state; rounding differences between finance SQL and application services; a payout batch that emits transfers before the batch row is committed (so a retry double-pays).
  • What test would protect it: an approval test on the nightly / weekly payout output for a captured input day, plus an idempotency invariant on (date, account-id) keys that retries hit the key, not the input.

Failure modes here are expensive. This trace touches money. Every bug here is also a finance support ticket and a provider trust ticket.

5. Reminder and closure. A background job sweeps open requests, unconfirmed bookings, past-due invoices, and review windows. Sends reminders, publishes reviews when the window ends, closes inactive requests, and sometimes moves records into "action required".

  • Tables touched: ServiceRequest, Booking, Invoice, Review, Notification.
  • Failure modes: a status reopened by admin re-firing a reminder that was supposed to be done; a review published into a window the ops team thought was extended; a reminder that fires before the payment state has settled.
  • What test would protect it: characterisation tests on the closure / reminder rules against fixed clock and fixed input; explicit ownership of the timing rule (one writer per timing decision).

This trace is easy to underestimate because it is not glamorous, yet it often decides whether the business feels disciplined or chaotic in practice.

Run, deploy, and test reality

Deployments happen every one to three weeks, usually with manual coordination and extra caution around finance or payment changes. The team has a build pipeline, but production release still depends on a senior engineer's manual judgement and a checklist in a wiki page. Database changes are often applied as reviewed SQL scripts rather than a fully trusted migration pipeline. A staging environment exists, but it is not production-faithful enough to settle every question. Restoring safe production-shaped data is possible, though awkward, and payment-side behaviour is only partly realistic in staging.

Local development works, but only if you know the rituals:

  • A sanitised database snapshot is required.
  • Secret configuration must be aligned manually.
  • Some jobs must be disabled locally or they start sending fake side effects in the wrong order.

A few tests exist around pricing, category matching, and report-level calculations. They are useful but far from complete. There are no end-to-end safety rails around the hardest business flows. What the team actually trusts today is a mixture of manual smoke testing, support knowledge, historical caution, and a fear of touching certain modules without reason.

The feared areas are clear: payment capture and payout code, finance exports, admin override paths, provider matching, reminder jobs, and any code that touches status transitions. The stable-but-boring areas are also clear: brochure pages, simple provider profile CRUD, low-risk configuration screens. Ignored parts include an old referral / campaign subsystem, legacy mobile API endpoints from an abandoned provider app, and duplicated admin pages that still exist because nobody has dared to prove they are dead.


The five System Report passes, applied to NaboWorks

A System Report is structured around twelve analysis passes; in any real engagement, five of them dominate. Mapped onto NaboWorks specifically:

Pass 1 — Repository orientation. Repo size, language counts, the two source projects with most behaviour (NaboWorks.Web and NaboWorks.Services), file-size discipline (worst offender: an admin controller > 3,000 lines, suspected god-object), README staleness, the folder regions where one developer worked alone for a year visible in naming inconsistency.

Pass 2 — Deployment and runtime topology. A single Linux VM-style host or Windows Server, IIS in front, SQL Server elsewhere, Hangfire-as-jobs, the parallel scheduled-executable hangover, the deploy checklist as the only true source of "what production really looks like", the absence of a fully production-faithful staging.

Pass 3 — Data layer. ER of the core five entities (customer, provider, request, booking, invoice/payment), table size and growth report (Notification, AuditLog, RequestStatusHistory are the unboundedly-growing tables), zombie tables surviving from old features, a list of columns used as flags / locks / soft-feature-switches that look innocuous but matter.

Pass 4 — Architecture-by-reading. The five trace paths above, each walked end to end, each producing a one-page document with route, auth point, validation point, services called, tables read/written, external services hit, error handling, transactional boundary. This is the heart of the engagement.

Pass 5 — The change-safety plan. What flows must be protected first; what tests must exist before the recommended package can land; the explicit refusal to mix refactor and new feature in the same release.

The other passes — code style, business rule mining, weird-behaviour catalogue, observability, AI collaboration strategy, etc. — are real and contribute to the report, but the engagement's load-bearing work is Pass 4.


Why "new frontend" is not just frontend work

The most common framing inside NaboWorks management is that they want "a modern frontend and a cleaner API". On the surface this sounds modest. In practice, very little of what makes NaboWorks NaboWorks lives in the frontend.

Razor views read EF entities directly. Business rules sit in controllers, in service methods, in stored procedures, in SQL views, in HTML helpers, and in scheduled jobs. State transitions are scattered across four or five surfaces. The same conceptual rule about provider eligibility can be expressed differently in a controller, a job, a SQL view, and an admin override. A frontend rewrite that ignores all of this is not a frontend rewrite. It is a quiet rewrite of business logic by a team that does not yet know the rules.

The honest version of "we want a modern frontend" is therefore: we want a system in which the frontend can change quickly and safely. That sentence sounds the same and means something completely different. The first one is a UI project. The second one starts with where the rules live.


The first question — what does "the same system" mean?

The buyer often starts by thinking the question is "how do we copy the app?" The more useful question is what does the same system actually mean? The answer is not a single rule. It sits in five buckets, each with a different commitment. For each bucket, the case study lists the items, the business owner who must confirm them, and the risk if they are changed accidentally.

Preserve exactly

Behaviours the new system must reproduce because they affect money, trust, legal responsibility, customer expectations, or operational continuity.

  • Money movement. Gross amount, commission, VAT treatment, provider share, payout hold, negative balances after refunds, recovery path after disputes. Owner: Head of Finance. Risk: immediate monetary discrepancy, provider mistrust, accounting confusion.
  • Eligibility to earn. Provider onboarding, document expiry, fraud / trust blocks, payout eligibility. Owner: Head of Provider Trust. Risk: a provider who is visible-but-not-payout-eligible silently becoming payable just because the new system simplified onboarding state.
  • Request-to-booking state semantics. The customer-visible labels may change later, but the business meaning of open request, assessment scheduled, offer available, booked, in progress, awaiting resolution, completed, refunded, paid out must survive. Owner: Operations Manager. Risk: notifications, admin permissions, customer self-service visibility, and the finance "is this payable?" decision all silently shift.
  • Admin audit and trust-sensitive actions. Manual credits, status reopenings, review moderation, payout holds, provider suspensions, refund approvals. Each leaves an auditable trail. Owner: Head of Operations. Risk: trust infrastructure becomes invisible.
  • Review publication and timing. The new system should preserve the blind-review and publication-window logic until deliberately changed. Owner: Head of Provider Trust. Risk: incentives shift across the marketplace before anyone notices.

Preserve behaviour, improve structure

External behaviour is the same; internal implementation is cleaner.

  • Request creation. Validation, persistence, matching, and notification logic separated cleanly.
  • Provider matching and offer handling. Routing logic moved out of controllers and out of half-hidden SQL. One of the most valuable refactors because the rule is important and the current shape is brittle.
  • Assessment scheduling and booking confirmation. State transition logic explicit rather than implied by scattered controller branches.
  • Notifications. Reminder timing, booking confirmation, invoice follow-up, review prompts: same business purpose, but orchestration in explicit workflows with idempotent handling rather than ad hoc side effects.
  • Reporting. Trusted totals stay trusted; outliers stay explainable. Implementation can move freely.

Change deliberately

Things the new system should improve, but only after explicit agreement.

  • Public intake UX. Simpler, more mobile-friendly, easier to experiment on. One of the commercial reasons the project exists.
  • Status wording. Customer-facing labels like "action required" or "converted" can become clearer later, provided the underlying business mapping is preserved.
  • Duplicate admin pages. A new system does not need to preserve duplication just because the duplication exists.
  • Document generation and export ergonomics. PDF layout, CSV export screens, secondary reporting UI can improve significantly without changing the business meaning of the data.
  • Provider dashboard navigation. Providers care more about clear action and payout trust than about preserving the exact menu structure.

Do not copy

Things that should not be replicated unless evidence says otherwise.

  • Dead campaign logic. The old referral / seasonal campaign subsystem partially survives in code but is no longer part of normal operations.
  • Abandoned mobile API fragments. Endpoints created for a discontinued provider mobile app should not be carried forward automatically.
  • One-off hacks for discontinued regions or customer segments. If the company once piloted a region or partner type and the code still contains conditionals for it, prove necessity before copying.
  • Fossilised admin conveniences. A button added years ago for a narrow ops workaround is not automatically a requirement for the future system.

Unknown — must be confirmed

Behaviours that may be business-critical but are not yet understood.

  • Legacy provider commission exceptions. Some long-standing providers may have grandfathered terms not captured cleanly in modern code.
  • VIP customer credit rules. Large landlords or property managers may receive manual goodwill credits or bespoke cancellation treatment that lives more in staff habit than in an explicit rule book.
  • Seasonal routing tweaks. Winter gardening, move-out summer peaks, or holiday staffing patterns may have changed who gets first access to certain demand without the logic being documented well.
  • Finance side spreadsheets. Reconciliation steps outside the main application that are treated as "normal" and therefore forgotten during technical walkthroughs.
  • Metrics definitions. The company may believe it tracks conversion or response time precisely while actually relying on legacy definitions tied to specific statuses or archived states.

These unknowns are not a sign of failure. They are the reason a serious replacement starts with rule mapping rather than with coding.


The questions the System Report has to answer

Before a single line of recommendation can be written, the System Report must surface evidence on each of the following. These are the load-bearing questions for NaboWorks specifically; some have working answers in the dossier, several do not.

About the product:

  • Which categories are truly fixed-price, and which are pretending to be? Working answer: simple cleaning and window-cleaning are fixed-price; painting, repair, gardening and handyman are request-first with a price band at best.
  • Which categories require assessment visits, and which can genuinely book instantly? Working answer: assessments are common in painting and repair categories; cleaning books instantly; the rest is hybrid.
  • Which states are customer-visible, and which are internal operational states only? Working answer: customer-visible set is small (received → reviewing → offers → booked → completed → refunded); the operational set is roughly twice that size and is what gates notifications, admin permissions, and finance treatment.

About money:

  • Which exact events create money, capture money, reverse money, hold money, or mark money as payable? Open. The System Report's job is to write that timeline down for the first time.
  • Which provider exceptions exist for commission, payout timing, or early access to demand? Open. Some grandfathered terms are suspected; none are documented.

About admin:

  • Which admin actions bypass normal validation today? Open. The hypothesis is "several"; the report's job is to enumerate them and rank them by risk.

About communication:

  • Which notification timings are business-critical rather than merely helpful? Working answer: payout notifications, dispute window notifications, and review-publication notifications are business- critical. The rest is helpful but tunable.

About reports:

  • Which reports do finance and management actually trust, and what makes them trusted? Working answer: a small subset is trusted because it is stable; the report should investigate whether the definitions match the trust.

About support:

  • Which support tickets reveal hidden business rules rather than simple UX problems? Open. Mining the helpdesk for ticket categories that recur is an early task in any serious engagement.

About code:

  • Which old modules are dead, and which are just rarely used? Working answer: the referral / campaign system, the mobile API fragments, and several admin pages are suspected dead. The report's job is to confirm.

About modernization mechanics:

  • Can authentication and session realistically be shared in a gradual extraction path? Open. This is the single biggest gating question for Path A.
  • If a gradual extraction path is chosen, which tables remain the source of truth during the hybrid period? Open. The default is "all of them"; that default is rarely safe.
  • If a controlled-twin path is chosen, which data can be mirrored or seeded safely and which must remain read-only until cutover? Open. This is the single biggest gating question for Path B.

About delivery:

  • What would count as accepted delivery at the beta milestone, and what would count as accepted delivery after the confidence month? See Commercial framing in detail.
  • If this case becomes a sample report, which findings should be ranked highest because they threaten money, trust, or operational continuity rather than because they look ugly in code? Open until the report is written.

A System Report that does not surface evidence on each of these is not finished.


Two serious paths

Two modernization paths are credible for NaboWorks. Both are alive until evidence is weighed. Neither is the correct answer in advance.

Path A — Gradual extraction (Strangler Fig)

A new façade or proxy sits in front of the old MVC application. A new ASP.NET Core shell can own selected routes, proxy unmatched routes back to MVC, and introduce cleaner API-backed slices over time. Microsoft's own incremental migration guidance is built around this shape; it is the mainstream conservative choice for production systems that must remain live during migration. Adapter packages exist precisely because real migrations pass through intermediate states where old and new code must coexist.

Where Path A is strong for NaboWorks. Lower immediate disruption. The business sees movement without condemning the whole old application at once. Read-only customer history, a cleaner customer request form, a modernised provider onboarding shell are all candidates for the first slice. It aligns well with a route-by-route proxy model and does not require committing to complete parity upfront.

Where Path A is fragile for NaboWorks. The hardest rules are not confined to views. They sit in shared tables, status transitions, notifications, finance exports, and admin overrides. A badly chosen first slice could leave the company with two frontends and one confused behavioural core. Authentication, session, anti-forgery, error handling, and configuration each need an answer before the first route flips.

Path A is the correct answer when: the System Report finds clear seams in the codebase, the public flows are decoupled from the heavy business rules, and the team has the discipline to keep the adapter layer temporary rather than letting it ossify into permanent infrastructure.

Path B — Controlled twin replacement

The new system is the future centre of gravity from early on, while the old system remains the live reference and source of operational truth until confidence is high. This is not a rewrite from scratch. It is a side-by-side replacement that starts by mapping the rules, choosing what parity means, creating a clean domain spine, importing or mirroring real-shaped data, and comparing old and new behaviour before any significant cutover.

Where Path B is strong for NaboWorks. If request creation, provider offer logic, admin overrides, and payout behaviour are too entangled to extract safely one route at a time, a clean spine is faster than disentangling the old one. The path is still incremental — increments accumulate inside a clean new system instead of being carved out of the old one. The old application remains live. Staff can preview the new one. Real scenarios can be exercised before transition. Data reconciliation, scenario comparison, and parity reporting become the safety mechanism.

Where Path B is fragile for NaboWorks. It costs more upfront. It requires the company to accept that month one and month two of the package look like rule mapping, environment building, and seeding — not visible product features. It demands honest commitment to the parity map: skipped questions become migration debt that surfaces in the confidence month.

Path B is the correct answer when: the report finds that the most important rules are scattered across surfaces (controllers, jobs, SQL, and admin overrides), no narrow first slice is genuinely safe, and the team is ready to invest in the parity map upfront in exchange for a cleaner future.


How the choice is actually made

Not from a diagram. Not from a preference. Not from a vendor's catalogue of services. From the System Report's evidence — access, testability, data realism, rule clarity, deployment, rollback, monitoring, stabilisation, and the minimum safety net required around the touched flows.

The report should likely conclude one of three things:

  1. A narrow gradual extraction is safe — but only after specific protection work around request creation, status mapping, and side effects. This is the conservative outcome.
  2. A controlled twin replacement is safer — because the old MVC system's business behaviour is too scattered to expose a trustworthy seam early. This is the strategic outcome.
  3. Neither path is safe yet — minimum stabilisation comes first: environment access, trusted staging, a data snapshot process, and enough rule clarity to know what the same system means. This is the unflattering-but-correct outcome that a serious diagnostician has to be willing to deliver.

A vendor that pre-commits to outcome 1 or 2 before reading the system is selling, not diagnosing.


Why the System Report comes first

The decision between Path A and Path B is the single largest decision NaboWorks will make for the next eighteen months. It is not made by judgement or by preference. It is made on evidence — and the evidence costs €3,000 of focused engineering, not €30,000 of committed building.

The report ranks findings by business relevance — money, trust, operational continuity, support burden, delivery speed, reliability, compliance, onboarding, and the ability to safely ship the next valuable change — not by technical ugliness. It names what can be safely built directly, what needs cleanup first, and what remains uncertain. Each recommendation must be made observable, testable, reversible, and confirmable; anything that cannot be made all four is something the report says cannot be safely changed yet, instead of dressing it up.

The single most important intake question — answered verbatim by the client and pinned to the front of the report — is:

"If this report achieves only one thing, what should that one thing be?"

For NaboWorks, the working answer is:

"We need to know whether to gradually extract the old MVC application or build a clean replacement beside it — and what the first safe package should be either way."

That answer drives every analysis pass that follows.


What the first package might look like

Whatever path the report recommends, the first package is bounded, scoped to the prerequisites the report names, and refuses to mix refactor and new feature in the same release.

If the outcome is gradual extraction

The first package is likely to include:

  1. Characterisation tests around request creation and provider offer submission. These freeze current behaviour as the spec.
  2. Extraction of one application seam. Often a BookingFlowFacade or RequestApplicationService — MVC controllers call the seam instead of doing validation, EF writes, email triggers, and state transitions inline.
  3. A façade or proxy layer in front of the old app, owning a small set of routes initially.
  4. One read-only API slice the new frontend can consume, chosen for low write-side risk (customer order history, provider profile viewing).
  5. An auth and session story for the hybrid period — if old and new must share auth, write that down explicitly and stress-test it.

If the outcome is controlled twin replacement

The first package is the Parallel Replacement Beta — a running new system on the easiest end of the parity map, with mapped rules, a clean spine, the first usable modules, seeded or mirrored data, and a beta environment staff can log into. See Commercial framing below for what "running" means at the beta milestone, or the dedicated package page for stage-by-stage deliverables and acceptance criteria.

If the outcome is stabilise-first

The first package is unglamorous and critical:

  • environment access (real, repeatable)
  • a trustable staging story
  • a safe data snapshot pipeline
  • characterisation tests around the most fragile flows
  • documented deployment steps that do not depend on one engineer's memory

Without those, both modernization paths are guessing. Stabilise-first is the package the team often wants the report to skip; it is also the package the team often most needs.

No matter which package is chosen, refactor work and new feature work do not share a release. Mixing them means a regression cannot be attributed to either.


Parity verification — what comparing old and new actually looks like

If the recommended path is the controlled twin, parity verification becomes the load-bearing safety mechanism. This is often under-described by vendors who pitch "clean rebuild" projects, so it is worth being explicit.

Three layers of parity:

  1. Output parity. For a captured input, does the new system produce the same output as the old? Examples: same invoice total for the same booking; same payout amount for the same job; same set of matching providers for the same request; same set of notifications fired for the same status transition.
  2. Workflow parity. For a sequence of events, does the new system produce the same end state as the old? Examples: a request created → assessed → offered → booked → completed → invoiced → paid out arrives in the same final state with the same audit trail and the same pending side effects.
  3. Operational parity. For a real day's traffic shape, does the new system handle it without producing observably different behaviour? Examples: notification volumes within a known window; payout-batch timing within a known window; admin-action distribution similar to the old system's.

How parity is actually verified:

  • Captured input fixtures. Real anonymised inputs from production — one day, one week — replayed into both systems and compared.
  • Reconciliation reports. For finance flows, comparing old and new outputs row-by-row and naming every discrepancy until each is classified as expected, bug-in-old, bug-in-new, or parity gap to fix.
  • Staff scenario testing. Support and ops run their hardest weekly cases against the new system and the old system in parallel, naming every behaviour difference.
  • Shadow traffic. Once confidence is high, real traffic is mirrored against the new system without affecting customers; outputs compared, not yet acted on.

The confidence month exists for this work, and it cannot be skipped without quietly downgrading the promise.


Commercial framing in detail

The numbers below are illustrative — a worked example of how the work might be staged if the System Report concludes one of the paths above. Pricing is not committed up front, and the only thing every NaboWorks-shaped engagement actually starts with is the System Report itself. Anything beyond it is decided after both sides have read it.

Stage 1 — System Report

  • Outcome: decide whether NaboWorks is a gradual-extraction case, a controlled-twin case, or a stabilise-first case.
  • Effort: 4–5 weeks of focused engineering, 4–6 weeks elapsed.
  • Price: €3,000.
  • Payment: €1,500 before the review begins; €1,500 on delivery of the report.
  • Accepted delivery: the written report, the focused-question list, and a proposed implementation package if one makes sense.

Stage 2 — If the outcome is Parallel Replacement Beta

  • Outcome: a running replacement candidate. Not a throwaway prototype.
  • Effort: approximately 3-4 months.
  • Price: approximately €30,000.
  • Payment: €10,000 to start; €20,000 on beta delivery.
  • Accepted delivery includes: mapped business rules and parity decisions; a clean application spine; the first usable modules (e.g., request capture, provider matching read-only); seeded or mirrored data covering core entities; a beta environment NaboWorks staff can log into; written documentation of what is preserved exactly, preserved structurally, deliberately changed, deliberately dropped, and still unknown.

Stage 3 — Parity and Transition

  • Outcome: the new system reaches production confidence.
  • Effort: approximately 3 more months.
  • Price: approximately €30,000.
  • Payment: €30,000 on accepted delivery at the end of the cycle.
  • Accepted delivery includes: migration scripts that run end-to-end against a snapshot; reconciliation reports across the trusted finance flows; staff scenario testing across the hardest weekly cases; deployment hardening; runbook for cutover; rollback plan; the open-question list resolved or explicitly deferred with business sign-off.

Where the visible asset shows up

  • Month 1–2. Rule mapping, environment, seeding, clean-spine scaffolding. Not visibly impressive. This is where commercial patience is required.
  • Month 3. Beta environment exists. Staff can log in. First modules visible.
  • Month 5. Replacement candidate runs with data and the main workflows.
  • Month 6. Confidence month — sanity checks, parity validation, bug-fixing, missing detail closure, acceptance.

The public promise is not "finished in six months no matter what". The public promise is: for a bounded, suitable system, the first package creates a real asset, and the final phase is reserved for proving the system rather than pretending proof is free.


The companion artefact

The full sample System Report for this scenario is published at /reports/naboworks. It mirrors the playbook structure: cover, table of contents, executive summary, engagement basis, evidence boundary, system in plain English, system in technical voice, ranked findings with plain and technical voices, change-safety plan, implementation strategy, recommended package, open questions and appendix.

The miniature widget on the System Report page is the same report at smaller scale — every page is the same data, just the size of a postcard.

The case study you have just read is the input that report is written from. When the report makes a finding about, for example, admin override paths bypassing validation, that finding traces back to the Admin override / support intervention trace path above. When it ranks a finding by business relevance, the rank is anchored to the "What an outage actually means" table and the parity buckets. When it makes a recommendation, the recommendation is constrained by the five System Report passes and the change-safety plan discipline.

The report is not a separate exercise. It is this case study, distilled into ranked findings and a recommended package.


Honest framing

A few things to be clear about.

The company is fictional. NaboWorks is a deliberately constructed composite of real ASP.NET MVC service marketplaces, built so the modernization decision can be reasoned through against a coherent world. There is no real client behind this page.

The pattern is not. The decision frame, the parity map, the choice between gradual extraction and controlled twin replacement, the framing that "new frontend" is rarely just frontend work — these come from operating ServiceByen.dk for 13 years through every phase of an ASP.NET MVC system: controllers full of business rules, Razor views reading EF entities directly, scheduled jobs that mutate the same tables as user actions, payment flows that cross stored procedures and controller code, admin actions that bypass validation. The story NaboWorks tells is the story most mature MVC marketplaces tell. The specifics differ; the shape does not.

A Smallbox engagement applying this to a client codebase would be a first. The discipline comes from operating an MVC marketplace through its entire life. The System Report would scope the work explicitly — gradual extraction, controlled twin, or stabilise-first — before any code is moved.

The strategic tension is preserved on purpose. A vendor that has already decided NaboWorks is a Path A case before reading the system is selling a service the company may not need. A vendor that has already decided it is a Path B case is making the same mistake from the other side. The whole point of this case study is that both options are intellectually alive until the evidence is weighed.

That is the honest version. The decision matters more than the narrative.

Want your product in this shape?

← All work