Skip to content

Solo Founder Product Engineering Handbook / Chapter 47

Refactoring After Evidence

Refactor only after evidence clarifies which workflows, data, tests, and operational paths deserve durability.

Monday’s Report Is the Boundary

The client-reporting product from the previous chapter has earned one commitment. Small agencies that complete source-field mapping return to send weekly reports, and some pay for pilots. When an import fails or an export arrives late on Monday, the founder must inspect a job in the database, repair its state, and try again from the console.

The reporting code is not the only ugly code in the product. Template rendering is copied between report types. Permissions assume one agency owner. Freelancer portfolio pages still share helpers with agency accounts. Billing state lives in two places. Any of those facts could begin a cleanup project.

Only one of them describes the work in front of the founder: a paying agency cannot finish the workflow it returns for, and recovery depends on private founder knowledge.

That consequence gives the refactor a boundary. The founder will make the path from source import through report delivery safe to retry and possible to diagnose without editing production data. The work will not create a universal connector framework, redesign every report type, or prepare the whole application for an imagined enterprise customer. Those areas may remain awkward.

Refactoring after evidence is selective commitment. It makes observed value easier to protect and change while leaving unresolved product questions cheap enough to answer honestly.

Write the Product Contract Before the Code Plan

“Clean up reporting” has no natural end. A useful plan begins with the behavior that has earned durability, the evidence behind it, and the result the founder expects outside the codebase.

For the agency product, the plan can fit in a small note:

EVIDENCE-BASED REFACTOR PLAN

Validated workflow:
  An agency imports source data, reviews a report, and delivers it each week.

Evidence:
  Six target agencies sent a second report; four are paid pilots.
  Import failures caused five manual interventions in the last two weeks.
  Two late reports produced support calls.

In scope:
  Import run states, safe retry, report assembly, export delivery,
  account-level diagnostics, tests for the validated path.

Out of scope:
  More source integrations, a report plug-in system, freelancer features,
  enterprise roles, a general job framework.

User risk:
  Duplicate delivery, missing report sections, incorrect access,
  or an import being applied twice.

Safety:
  Characterization tests, idempotency key, staged account migration,
  old-path fallback during rollout, database backup and rollback note.

Stop when:
  A failed import can be diagnosed and retried from the admin view;
  the same input cannot create a second report or second delivery;
  one new report section can be added without changing delivery code.

Observe afterward:
  Founder interventions, time to diagnose, late-report incidents,
  and lead time for the next validated report change.

The numbers are modeled, but their role is concrete: they expose why this workflow deserves engineering time and provide a baseline against which the result can be judged. In a real plan, use the product’s actual observations, including uncertainty. “Several customers complained” is weaker than a short list of incidents and affected accounts.

An evidence-based refactor plan showing a messy prototype path narrowing into a validated core workflow and then becoming a durable system, with checkpoints for evidence, scope, safety, and leverage.
Evidence narrows the path worth reinforcing; scope, safety, and leverage keep the reinforcement from becoming a general rewrite.

The out-of-scope lines are as important as the work list. They protect discovery from architecture. If a second source integration appears during the refactor, the founder can still test it manually instead of expanding the project around a variation that has not repeated.

Preserve What Users Already Depend On

Refactoring changes structure while preserving intended behavior. That sounds simple until the prototype’s actual behavior is scattered across controllers, jobs, templates, database defaults, and manual recovery steps.

Before moving code, trace one real report from start to finish:

  1. an agency uploads or connects source data;
  2. the system records an import attempt;
  3. fields are mapped and validated;
  4. report sections are assembled;
  5. an account manager reviews the result;
  6. the system renders and delivers the export;
  7. the agency and client can see what happened.

For each step, write down the input, durable state change, output, and visible failure. Include the founder’s hidden actions. A console retry is part of the current system even though no customer can see it. So is the note that says which accounts must never be retried after a partial export.

Then place tests at the boundaries where a mistaken change would break trust. The first tests need not describe every helper. They should pin down the behavior that current users have earned:

  • the same import request cannot apply its data twice;
  • a report cannot be delivered before required sections pass validation;
  • a retry resumes or safely repeats work without creating a second delivery;
  • an account manager cannot read a report outside the permitted workspace;
  • the exported totals and reporting period match the reviewed report;
  • a failed run retains enough state to explain where it stopped.

Some of these will be characterization tests: they record what the system does before its internals are improved. Others establish a rule the prototype never made explicit. Mark that difference. Quietly preserving a bug is not fidelity, but silently changing customer-visible behavior during a structural refactor is not discipline either. When intended behavior is unclear, decide it as a product question and communicate any material change.

Give the Workflow One Honest State Model

The prototype currently treats “generate report” as one action. In reality, the report moves through states: source received, mapping required, ready to assemble, under review, ready to deliver, delivered, or failed at a named step. The console procedure exists because those states are implicit.

The smallest useful structural move is to put those transitions in one place. A report run should accept a valid command, record its current state, perform the next operation, and record either the resulting state or an inspectable failure. Controllers and background workers can request transitions; they should not each invent them.

This boundary creates leverage in several directions at once. A retry can ask the run what work remains. The admin view can show the same state the worker uses. Tests can exercise transitions without rendering a page. Logs can attach to a stable run identifier. A future collaborator can find the business rules without reconstructing them from several request handlers.

Do not turn that success into permission to extract a framework. The founder needs a reporting boundary, not a generic workflow engine. Name modules after the product domain—ReportRun, SourceMapping, ReportDelivery—and keep their interfaces narrow. An abstraction earns another use case only when the use case arrives.

The same restraint applies to copied report templates. If the copies contain the same delivery mechanics, move those mechanics behind the new boundary. If two report sections merely look similar but still change for different customer reasons, duplication may remain cheaper than a shared configuration language. Repeated syntax is not always repeated product meaning.

Change the Data Without Gambling With It

The state model may require a new report_runs record rather than a collection of booleans and timestamps on the report. Customer data turns this from an ordinary code move into a migration.

Separate the transition into reversible stages:

  1. add the new records without removing the old fields;
  2. write new activity to both representations if doing so can be kept consistent;
  3. backfill a small set of accounts and compare the derived states with the old history;
  4. read from the new model for the founder’s own or a test account;
  5. expand the read path gradually while watching mismatches and failures;
  6. stop writing the old representation only after the new path has proved reliable;
  7. remove old fields in a later change, after the rollback window closes.

Dual writes are not automatically safe. A request can update one representation and fail before updating the other. If the database supports a transaction that covers both writes, use it; otherwise record enough reconciliation information to find divergence. For some products, a one-time migration during a brief maintenance window is simpler and safer than weeks of dual state. The right method depends on data volume, availability needs, and the cost of inconsistency.

Before touching customer records, take a usable backup and verify how it would be restored. A rollback note should name the code version, schema action, and data consequence—not merely say “revert if needed.” Rolling application code back after new records have been written may require compatibility in both directions.

The product model should guide the schema. The evidence supports agencies, workspaces, client reports, review, and delivery. It does not yet support an elaborate hierarchy of organizations, divisions, portfolios, and configurable approval chains. A cleaner schema that encodes unmade product decisions is still premature architecture.

Make Failure Legible and Recovery Ordinary

Once report runs have explicit states, observability can answer a support question rather than decorate an infrastructure dashboard.

Give each run a stable identifier. Record the account, source, reporting period, transition, outcome, and a safe error classification. Exclude secrets and unnecessary customer content. The admin view should show the event trail, the last successful step, whether retry is allowed, and the action the founder can take. An alert should fire only when the run is unlikely to recover automatically and delay threatens a customer commitment.

This is also operational debt payment. The old system detects failure when a customer writes in and recovers through a console session. The new system detects a stuck run, preserves the evidence needed to understand it, and offers a bounded recovery action. The code change matters because the founder no longer has to remember a fragile ritual.

Measure that result in the unit the problem consumed. If diagnosis took thirty minutes and a database query before the change, track whether the next few incidents can be understood in five minutes from the event trail. If failed runs required five manual interventions in two weeks, track interventions rather than the number of new log lines. If alerts wake the founder for failures that recover on their own, the observability has created a new attention tax.

Delete Before You Generalize

The validated agency path still shares helpers with a freelancer portfolio feature that has weak retention. Extracting shared modules before deciding the feature’s future would force the new reporting boundary to preserve old concepts.

Remove the dead route, its settings, jobs, tests, analytics events, documentation, and support promise if product evidence supports ending it and affected users have been handled. If immediate removal would surprise an active user, first stop new adoption, provide export or notice as appropriate, and set a retirement date. Deletion is a product change even when it simplifies a refactor.

Only then inspect what remains. The useful module boundaries are usually easier to see after abandoned variants stop demanding representation. This is one reason refactoring and product judgment cannot be separated: dead product surface creates dead architectural constraints.

Refactor, Rewrite, or Leave It Alone

An incremental refactor is the default here because agencies are already using the workflow and the existing path can be kept alive while its internals move. Each step can preserve or compare behavior, serve current users, and leave the founder available for support and discovery.

A rewrite may be justified when no safe seam exists, the current representation fundamentally conflicts with the validated workflow, and the product can support a controlled replacement. Even then, “rewrite” should describe a migration, not a disappearance. Run old and new report generation for selected accounts, compare outputs, migrate one account, retain a fallback for a defined period, and expand only when the evidence supports it.

Leave code alone when its product question remains open or its awkwardness produces no meaningful cost. The hard-coded single-source importer may still be the correct constraint while every validated agency uses that source. A plug-in system would consume time without reducing current risk or enabling the next named experiment.

These choices can coexist in one product: refactor the report-run state, rewrite a dangerously ambiguous permission check behind a compatible interface, delete the freelancer surface, and leave the source adapter ugly. Evidence does not bless or condemn a codebase. It selects commitments.

Stop When the Founder Has Gained Leverage

The refactor is complete when the plan’s observable conditions are true, not when the surrounding code has stopped suggesting improvements.

Run the validated workflow end to end, including a forced failure and retry. Compare exports from the old and new paths where both exist. Confirm that access checks hold at workspace and report boundaries. Restore a representative backup in a safe environment. Ask someone unfamiliar with the change to follow the runbook or run the main path from a clean setup if preparation for collaboration was part of the evidence.

Then look outside the implementation:

  • Can the next validated report change be made without disturbing delivery?
  • Can the founder explain and recover a failed run without querying production tables?
  • Have late-report incidents or manual interventions changed?
  • Can another person run, test, and release the core workflow without private oral history?
  • Does the design have enough headroom for the next evidenced load, without pretending to solve every future scale problem?

Preparing for scale means removing the next observed constraint: perhaps report jobs contend for one worker, exports exhaust memory, or one account can delay all others. Measure that constraint and design for the next credible range. Service decomposition, queues, caches, and sharding are not maturity badges. They become relevant when the validated workflow and its measured behavior require them.

The remaining mess belongs on a short debt note with a review trigger. Do not pull it into the refactor simply because the code is open and the context is fresh. Close the project, return to customers, and see what the durable path now allows the product to learn.

The founder has not made the whole application permanent. One workflow has earned safer state, clearer boundaries, recoverable failure, and a structure another person can enter. Everything else remains answerable to evidence—including the product direction itself. The next chapter takes up the harder case: when the evidence says that direction should change.