Delivery schedule · ref CFX-0000 · valid 30 days
One lift. Shipped, handed over, done.
A fixed-scope port for a ColdFusion app with no future beyond maintenance. Scope and acceptance criteria in writing before anything starts, and a finish date measured off the real work rather than padded to look safe.
| Stage | Gated by | Accepted when |
|---|---|---|
| Inventory delivered | Read access to the code and the logs | File list, route map and job list agreed in writing |
| Scope frozen | Your sign-off on the list | In and out of scope agreed; nothing widens after this without a change |
| First unit accepted | Scope frozen | One route read, ported, diffed clean, and through the loop end to end |
| Date committed | The first unit — which is what makes throughput a measurement | A dated finish you can hold us to, from the loop we have just run |
| All units accepted | The loop draining | Every in-scope unit diffs clean, or carries a decision you signed |
| Cutover | A window you choose | 72 hours on live traffic with no rollback triggered |
| Handover | Cutover held | Source, tests, runbook and pipeline transferred and demonstrated |
Nobody can price the exit, so it never gets funded
"Migration" sits in the plan as an unbounded number, and unbounded numbers do not get approved. So it gets deferred again, and the estate ages another year.
You do not have a team to run a programme
Incremental migration assumes engineers who can carry a second system alongside the first for a year. If that is not your situation, the method that is meant to reduce risk is the one adding it.
The app works. It just cannot stay where it is.
Nothing is on fire. The runtime is unsupported, the people who understood it have moved on, and every year the same decision gets postponed for the same reason.
Frozen before anything starts, and written down in this shape. Everything marked out of scope can be added, but it changes the schedule rather than disappearing into it.
- Every template and component reached in the last 90 days (in scope)
- Scheduled jobs, including the ones nobody remembers commissioning (in scope)
- Database access, ported to parameterised queries throughout (in scope)
- Characterisation tests generated from current behaviour (in scope)
- Deployment pipeline and rollback runbook (in scope)
- Security findings surfaced during the port, written up (in scope)
- Code with no traffic in the inventory window (out of scope)Listed and handed to you rather than ported. Usually the largest single saving.
- New features, redesigns or workflow changes (out of scope)A port that also changes behaviour cannot be verified against the original.
- Ongoing support after handover (out of scope)Available separately. The deliverable is built so that it is optional.
- Data migration between database engines (out of scope)Scoped separately when the schema is moving as well as the code.
A single lift is not right for every estate, and we would rather say so now.
Whether one cutover is safer than a sliced migration comes down to how much of the application is genuinely live, and to what happens if any of it is subtly wrong. Size decides the effort; traffic and business criticality decide how much proving the cutover needs.
- The application is stable — no active feature roadmap competing for the same code
- The live surface is modest once dead code is excluded, which the inventory establishes
- You would rather own the result outright than run a migration programme
- There is a definable "done", and everyone involved agrees what it is
Over the line on any of these and we will tell you at the schedule stage, before there is anything to pay for. An estate that needs a sliced migration needs one; quoting a lift for it would just be selling you the cheaper-sounding thing.
- The date comes from throughput, not a guess
- Measured
- Cutover, not a migration you live inside
- One
- Source, tests, runbook and pipeline
- Yours
Source
In your repository, in a mainstream stack, with the commit history of the port intact.
Tests
The full characterisation suite, runnable in your pipeline, documented.
Runbook
Deploy, roll back, monitor, and what to do at three in the morning.
Findings
Everything the port surfaced about the old system, whether or not we changed it.
Not tests, then porting, then a cutover. Reading a unit and rewriting it are the same piece of work, and units move through independently — which is where the speed comes from, and why the finish date is arithmetic rather than a guess.
Read the old behaviour
For each unit, what it actually does — driven by real request patterns from your logs, not by a specification nobody wrote. This becomes the yardstick, and it is only trustworthy once it passes against the system you already have.
Write the new one, at the same time
Not afterwards. Reading a unit and rewriting it are the same piece of work, and holding them together is what stops the port drifting from the thing it is meant to replace.
Diff, and decide what differs
Both are run and compared. Anything that differs is a finding rather than a bug fix, because a silent improvement is an unannounced behaviour change. You decide: preserve it, or change it deliberately.
Drain the loop, then cut over
Units flow through independently — one can be in the diff while another is still being read. When the last one is accepted, one switch, watched, then handover and we are gone.
- Scope frozen before anything starts
- We inventory what is actually running — templates, components, scheduled jobs, integration points — and the list is agreed in writing before a line is ported. What is out is as explicit as what is in.
- Acceptance criteria you can hold us to
- Each stage has a written condition for being done. The port is accepted when a characterisation suite built from your current behaviour passes against both systems, not when we say it looks finished.
- One cutover, not a year of living in two systems
- No dual-run infrastructure, no traffic splitting, no shared session state between a legacy runtime and a new one. Those costs buy you the ability to keep shipping features, which a frozen app does not need.
- Handover, and then we are gone
- Source, tests, runbook and the deployment pipeline transfer to you. No retainer, no lock-in, no proprietary tooling you would need us to maintain. If you never call us again, everything still works.
What does "fixed scope" actually mean here?
The inventory produces a list of files, routes and scheduled jobs, and that list is the contract. We do not port what is not on it, and we do not quietly widen it. If something is discovered mid-project that genuinely was not visible in the inventory, it is raised as a change to the schedule rather than absorbed silently or used to reopen the commercials.
What happens when you find more than expected?
It depends what kind of more. Dead code and unreachable branches shrink the job and we say so. Hidden complexity inside something already in scope is ours to absorb, which is what makes the scope fixed rather than indicative. Genuinely new surface area — an integration nobody mentioned, a second database — is a scheduled change with its own dates, agreed before work continues.
How do you know the new system behaves the same?
Every unit carries its own characterisation tests, derived from how your application actually behaves under real request patterns from your logs rather than from a specification nobody wrote. Those tests have to pass against the system you already have before they are trusted for anything — a test that only passes against the new code is measuring our opinion. Then both are run and compared. Where behaviour cannot be preserved, or where the legacy behaviour turns out to be a bug, it is written up as a finding and you decide. We do not silently improve things: a silent improvement is an unannounced behaviour change.
Do you write the tests first and then start porting?
No, and the diagram above exists because that assumption is what makes people think a port takes three times longer than it does. Reading a unit and rewriting it are the same piece of work and happen together, unit by unit. Units then move through independently — one can be in the diff while another is still being read. There is no phase where a team is only writing tests, and no phase where code is being written with nothing to check it against.
How long does it take?
Inventory and scope are days. After that it is arithmetic: units remaining multiplied by how fast the loop is actually running, which we know as soon as the first unit has been through it end to end. That is the point where we commit to a date, and it is why there is not one printed on this page — a number published before the work started would be padding, and padding is how "migration" came to mean eighteen months. For a small frozen estate the whole thing is frequently a couple of weeks. If it can be done in two weeks it will be done in two weeks.
Can you do this while we keep shipping features?
Not well, and if that is your situation this is the wrong offer. A single lift assumes the application is stable enough to freeze for the duration. If you have an active roadmap you want an incremental migration that runs alongside it, which is a different shape of engagement and a longer one. We will tell you that rather than sell you this.
What if our estate is too big for one lift?
Then the schedule stage says so and we do not quote a lift. Above a certain size a single cutover carries more risk than it removes, and the honest answer is a sliced migration. That is a real threshold based on how much of the codebase is actually live, how many routes are served, the traffic they carry and what breaks if any of it is subtly wrong — not on a page count.
What do we get at handover, and what do we need afterwards?
Source, the full test suite, a runbook covering deployment and rollback, and the build pipeline. Nothing proprietary, nothing licensed from us, no runtime that only we understand. The target stack is mainstream on purpose so that hiring for it is ordinary. If you want us afterwards we are available, but the deliverable is built so you do not need to be.
Why is there no price or date on this page?
Because both would be made up. A price band published without seeing your estate is either a floor we quote up from or a number we walk back, and a duration published the same way is padded so it can be met. What is published instead is everything that usually is not: what is in scope, what is out, the order the work happens in, and exactly what makes each milestone acceptable. Those we can commit to without seeing your code. The price and the date follow the inventory, and the inventory is quick.
What if the inventory says we should not migrate at all?
Then we say that. A small, stable application on a supported runtime with nobody struggling to maintain it does not need this, and we have told people so. If an assessment can only ever land on the expensive answer, it was a sales process wearing an assessment costume.
Get the schedule for your estate
Two fields to start. You get back either a dated schedule with acceptance criteria, or an honest note that yours is too big for a single lift and what to do instead.