Skip to content
Aug 14, 2026ยท8 min read

ColdFusion migration starts before the licence expires

Plan a ColdFusion migration that preserves live traffic, separates CFML behavior from runtime quirks, and retires the licence on evidence.

ColdFusion migration starts before the licence expires

A decision not to renew ColdFusion is a deadline, not a migration plan. The executable problem is to preserve every behavior that users and adjacent systems depend on while the CFML runtime still gives you a trustworthy reference. If the team starts by converting tags into another language, it will faithfully reproduce the least useful part of the system and miss the contracts hidden in database results, session state, scheduled jobs, file drops, and error handling.

A safe ColdFusion migration keeps the existing application serving traffic while a replacement takes routes and jobs in measured slices. The order matters: inventory the live behavior, pin down the application boundary, isolate database semantics, move non-visual entry points, then transfer reads and writes route by route. The old server leaves only after recorded traffic proves parity and the rollback path has stopped being useful.

Treat non-renewal as a commercial boundary

Non-renewal tells you when the organisation wants to stop buying the product. It does not, by itself, tell you what an installed server will do on that date. ColdFusion editions and licensing models differ, and Adobe changed the 2025 release from serial-key licensing to subscription-based Named User Licensing and Feature Restricted Licensing. Older installations may sit under different terms. Read the order, licence agreement, deployment records, support dates, and activation method for the exact version in production. Do not let an engineer infer legal rights from whether a process still starts.

Separate four dates on the programme board: the renewal decision, the end of vendor support for the installed version, any activation or entitlement event, and the date on which production can run without CFML. They may not match. Ask procurement or counsel to resolve usage rights, and ask operations to test activation behavior in an isolated clone. Neither answer replaces the other.

Keep the licensed production estate stable during the exit. Freeze runtime upgrades, JVM changes, datasource driver swaps, and opportunistic framework cleanups unless security forces the issue. Every simultaneous change weakens your reference. Apply supported security updates, restrict administrator access, retain installation media and configuration exports where your terms allow it, and record the exact JVM, connectors, hotfixes, mappings, datasources, scheduled tasks, and external services.

Do not promise that the server will fail at midnight, and do not promise that it will run forever. Both claims are guesses until someone reads the applicable terms and tests the actual entitlement mechanism. The useful deadline is the one your evidence supports.

Inventory behavior, not file extensions

A directory count of CFM and CFC files is not a system map. Start from production entry points and trace what each one reads, changes, emits, and calls. A small template can trigger an include chain, invoke a CFC, mutate session scope, run SQL through a named datasource, write a PDF, send mail, and redirect through an error handler. Its line count says nothing about migration risk.

Build a route and job ledger with one row per observable entry point. Capture the HTTP method and path, authentication state, important request fields, response status and content type, cookies changed, database tables read or written, files or messages produced, external calls, and the owner who can judge the result. Add scheduled tasks, remote CFC methods, web-service endpoints, inbound file processors, administrator-created mappings, and manual operations that never appear in source control.

The awkward dependencies usually live outside CFML. ColdFusion Administrator may contain datasource credentials, mail settings, JVM arguments, sandbox rules, custom tag paths, scheduled tasks, and web-server connectors. Application.cfc may establish per-request behavior, but server configuration can still change what that code means. Export configuration where possible, then make a human-readable inventory. A screenshot is poor evidence because nobody can diff it reliably.

Log actual use before deciding what to migrate. Record route templates, status codes, request duration, authenticated role, and a privacy-safe request fingerprint. For jobs, record start, finish, outcome, and business artifact. A page absent from access logs may still run once at year-end; ask the business before declaring it dead. Delete only after an owner accepts that its behavior has no consumer.

Make Application.cfc the first boundary

Application.cfc defines the lifecycle that the replacement must either reproduce or deliberately change. Read onApplicationStart, onSessionStart, onRequestStart, onRequest, onRequestEnd, onError, and missing-template handling as one control flow. Then inspect Application.cfm and OnRequestEnd.cfm because older areas may still depend on them. Adobe's CFML Reference states that when Application.cfc is present, ColdFusion ignores Application.cfm and onRequestEnd.cfm in that application context. Directory placement therefore changes which lifecycle code runs.

Map every scope by lifetime and owner. Application scope is process-level shared state, session scope is user state, request scope is transient, server scope can cross applications, and variables scope changes meaning inside templates and components. The replacement does not need matching scope names, but it must preserve externally visible behavior such as session expiry, locale selection, authorization, request correlation, and cached reference data.

Do not carry process memory across the migration boundary by accident. If a CFC stored in application scope caches data and the new service updates the same records, stale reads can survive long after cutover. Give each cache an explicit source, expiry rule, invalidation path, and owner. During dual running, disabling a questionable cache is often safer than recreating its quirks.

Turn lifecycle behavior into middleware and services before converting presentation. Authentication, tenant resolution, transaction correlation, exception mapping, and response headers belong at the replacement's edge. Once these rules have named tests, a migrated route cannot silently bypass them. This boundary also gives the proxy one clear place to attach the same request identifier to old and new responses.

CFCs are several contracts wearing one extension

A CFC can be an internal object, a remotely callable endpoint, a stateful application singleton, or a thin wrapper around SQL. Treating every .cfc file as a future class preserves the file layout while hiding the public contract. Classify each component by how callers reach it and which state it owns.

For remote methods, record method name, accepted argument forms, defaults, authentication checks, serialization format, HTTP status, and fault shape. CFML callers often tolerate case differences and flexible types that a typed TypeScript handler will reject. A return value that looks like an array in CFML may serialize with query-specific column and data structures depending on settings. Capture the bytes seen by real callers, not the object shown by a debugger.

For internal CFCs, follow call sites before assigning a target design. A component with createObject calls scattered through templates may really be a request service. One placed in application scope may need concurrency control. A method marked remote may have no external consumer and should not become a public API merely because the source allowed it. Exposure is a behavior to verify, not an instruction to preserve.

Define replacement contracts in plain request and response fixtures. For example:

{"request":{"method":"GET","path":"/account/orders","sessionRole":"buyer"},"response":{"status":200,"contentType":"text/html; charset=UTF-8","setCookieNames":[],"bodyNormalizers":["csrf-token","generated-at"]}}

That fixture does not dictate the new architecture. It names what parity means and identifies fields that may vary. Add negative fixtures for missing sessions, malformed arguments, duplicate submissions, and dependency timeouts. Error behavior has consumers too.

Tag soup must be separated before it is translated

Mixed CFML templates contain at least four concerns: request control, business decisions, data access, and HTML rendering. A mechanical tag-to-syntax rewrite tangles the same concerns in a new language. The safer move is to identify the decision points and response contract, then implement them behind the existing route boundary.

Take a page with cfparam defaults, an authorization include, two cfquery blocks, a cfloop, cfoutput escaping, and a redirect after a form post. Its contract includes more than the final markup. It includes defaulting rules, redirect status and location, double-submit behavior, query ordering, null display, encoding, cookie changes, and perhaps the exact validation message expected by an automated client. Write those observations down before touching it.

Move pure rendering last within a route. First extract or reproduce the request model and database operations. Then render a stable view model in the new client or server template. This prevents a React or TypeScript UI from reaching back into undocumented query objects. It also makes HTML comparison practical because volatile tokens can be normalized without hiding differences in business data.

Do not standardise every screen during the exit. A redesign changes navigation, validation, accessibility behavior, and support procedures at once. It is popular because new screens look like visible progress; it is wrong when licence retirement is the binding constraint. Preserve the user journey, retire the runtime, then improve the interface with separate acceptance criteria.

The query object is part of the application contract

Finish before renewal wins
Every CodeHero rewrite is delivered in under 30 days, including ColdFusion source systems.

ColdFusion's query layer is more than a SQL transport. Adobe's data-access documentation describes cfquery as returning a query object with record data and properties such as RecordCount, ColumnList, SQL, Cached, SQLParameters, and ExecutionTime. Templates can iterate it, address columns as arrays, depend on case-insensitive names, run Query of Queries, or serialize it. A PostgreSQL driver returning rows does not automatically reproduce those semantics.

Inventory every datasource and classify each query as a read, write, transaction participant, stored procedure, Query of Queries operation, dynamic SQL builder, or cache-dependent query. Locate cfqueryparam gaps, but do not mix a broad security rewrite with parity work unless exposure demands it. Parameterization can change implicit conversions and query plans, so it needs its own tests.

Create a database adapter that returns explicit domain records rather than emulating a universal CF query object. At the old boundary, capture column names, ordering, null values, numeric precision, timestamps and zones, row count, generated keys, and exception behavior. At the new boundary, compare those fields before a template or API sees them. Preserve ordering only when SQL specifies it; an accidental database order is unstable and should become an explicit ORDER BY if callers rely on it.

Adobe documents QueryExecute as accepting SQL, parameters, and options, with either arrays or structs for parameters. That is useful during preparatory refactoring because a named function can expose SQL and bindings cleanly. It does not solve migration on its own. The hard contract sits in result shape, transaction boundaries, datasource configuration, and the code that consumes the query.

Use a small contract record for each important operation:

operation: findOpenOrders
inputs: customerId integer, cutoff timestamp UTC
reads: orders, order_items
ordering: orders.created_at DESC, orders.id DESC
nulls: shipped_at remains null
precision: total_amount decimal(18,2), never float
errors: missing customer returns empty rows; unavailable DB returns dependency error

That record is deliberately boring. It prevents the common failure where the new route returns the right rows in a different order, rounds money through a floating-point value, or converts a database null into an empty string.

Move jobs and non-visual endpoints before busy pages

Scheduled jobs, inbound processors, feeds, and narrow remote methods are good early slices because their inputs and outputs are easier to record than an interactive page. They also expose server configuration that a page-first plan overlooks: filesystem paths, service accounts, mail relays, proxy rules, locale, time zone, and retry behavior.

Do not move a scheduled task by copying its cron expression alone. Determine whether ColdFusion prevented overlap, how operators retried failure, where output was logged, which working directory it assumed, and whether a partial run could be repeated. Give the replacement an idempotency key or checkpoint where the old job had an accidental single-process guarantee. Run it in shadow mode against copied inputs before giving it authority to write.

Remote CFC calls and feeds make good proxy candidates. Route a sampled or internal cohort to the replacement, compare response bytes after approved normalization, then increase authority. If the endpoint writes, shadowing the write can cause duplicate effects. Compare on a restored database, or let only one implementation commit while the other produces a proposed change set.

This phase tests deployment, secrets, observability, rollback, and ownership with limited user exposure. If the team cannot operate one migrated job through a failure, it is not ready to move the checkout, account, or reporting routes.

Put a proxy in charge of the migration order

Handle the million-line tree
CodeHero reads codebases over a million lines and processes every language in the tree in parallel.

A reverse proxy or existing load balancer should decide which implementation owns each route. DNS is too coarse, and a flag buried inside CFML still requires the old runtime for every request. Route ownership needs one visible table that operations can change quickly without redeploying both applications.

Choose slices with explicit boundaries: exact paths, methods, hostnames, tenant cohorts, or stable user cohorts. Avoid percentage routing for stateful writes until both sides share compatible sessions and data behavior. A user who submits a form to one implementation and receives the redirect from another can expose token, flash-message, and cache differences that isolated route tests miss.

A practical order is:

  1. Static assets and health endpoints that have no business state.
  2. Scheduled jobs and narrow read-only APIs with recorded fixtures.
  3. Read-only pages backed by the new database adapter.
  4. Write routes with idempotency, transaction, and rollback tests.
  5. Authentication, session creation, uploads, exports, and the remaining cross-cutting routes.

Keep the database authoritative in one place through each slice. Dual writes look reassuring but create reconciliation work precisely when the team needs a clean rollback. Prefer one writer, compatible schema changes, and change capture or audit comparison. If a target Postgres schema differs from the source database, introduce it behind an adapter and migrate ownership table by table, not by letting two unrelated models accept production writes.

Recorded traffic turns parity into evidence

Unit tests prove chosen examples; recorded production traffic shows what callers actually send. Capture requests and outcomes with privacy controls, replay them against the replacement in an isolated environment, and compare status, headers, normalized body, database effects, and emitted side effects. Do not copy secrets or personal data into an unmanaged test store. Tokenize or redact fields while preserving the distinctions the code uses.

Give every mismatch a category: intentional change, volatile field, source defect accepted temporarily, or replacement defect. Normalizers should be narrow and reviewable. Ignoring every timestamp-shaped string or sorting every array can make a broken response appear equal. Normalize a named CSRF field, generated identifier, or clock value only when the contract permits variation.

A reproducible HTTP check can be simple:

curl -sS -D old.headers -o old.body -b session.txt https://old.internal/orders/1042
curl -sS -D new.headers -o new.body -b session.txt https://new.internal/orders/1042

The useful artifact is the comparison report, not the commands. Store request fingerprint, old and new status, differing headers, body comparison result, database change summary, side-effect summary, and reviewer decision. Require clean evidence for representative successes, authorization failures, invalid inputs, empty results, retries, and dependency failures.

CodeHero uses this model at codebase scale: the platform reads the whole mixed-language tree, and a parity harness checks the replacement against recorded production traffic. That is the right standard whether you build the harness yourself or use ours, because licence retirement should rest on observed behavior rather than converted file counts.

Cut over writes with rollback still intact

A route is ready to own writes when the replacement has proved data semantics and the old path can resume without repairing incompatible state. Use expand-and-contract schema changes: add compatible columns or tables, deploy readers that tolerate both forms, move the writer, verify, and remove old structures only after rollback expires.

Sessions need an explicit decision. You can share a session store and cookie contract, translate sessions at the proxy, or force a controlled reauthentication when a cohort moves. Sharing is convenient only if serialization, expiry, rotation, and encryption agree. Reauthentication is often cleaner, but schedule it and protect in-progress forms. Never discover the session strategy during the first production write cutover.

For uploads and generated files, define ownership outside local server disks. Confirm filename rules, permissions, virus scanning if present, retention, and atomic visibility. A replacement that writes to a container filesystem while CFML reads a shared directory will pass request tests and lose files after deployment.

Set rollback triggers before cutover: parity defect in a protected workflow, error-rate change, queue growth, database invariant failure, or operator inability to reconcile a side effect. The trigger should name who can revert and what happens to writes already accepted. Rolling traffic back without reconciling committed data is not rollback.

Move one bounded write family at a time, keep the old code deployable, and resist cleaning the source schema. Once the evidence window closes and owners accept the route, remove its ability to write from the old application. That turns rollback from a vague hope into a deliberately expiring option.

Make operations independent of the old console

Prove each migrated behavior
Recorded production traffic tests response and data parity before the ColdFusion runtime leaves.

The migration is incomplete while an operator needs ColdFusion Administrator to understand or recover the service. Move health checks, structured logs, request identifiers, job history, feature controls, and dependency status into the normal operational system before the last routes leave. Preserve the meaning of alerts, but do not preserve a console merely because people know where its buttons are.

Write a runbook for each migrated workload while the person who operates the CFML version can still compare it. The runbook should state how to deploy, pause, retry, roll back, inspect a failed request, reconcile a partial side effect, rotate a secret, and confirm database connectivity. Test it during a controlled failure. A document copied from an old server build sheet does not count if the commands no longer reach the component that owns the work.

Configuration needs the same treatment. Named datasources, mappings, mail settings, time zones, and JVM properties often act as hidden inputs. Put replacement configuration under review, separate secrets from ordinary values, and make startup fail clearly when a required value is absent. Record which old setting maps to which new setting and which setting was intentionally discarded. That last category matters because otherwise an unused administrator value can delay shutdown while the team searches for a consumer that never existed.

Keep observability comparable across both implementations. Use the proxy request identifier in application logs, database audit records where available, queued work, and parity reports. Match business outcomes rather than forcing identical log messages. Operators need to answer whether order 1042 was accepted once and whether its notification was sent, not whether two runtimes chose the same stack-trace wording.

Run a restore and recovery exercise before retiring the reference system. Restore the replacement data and configuration into an isolated environment, replay a small accepted traffic set, and confirm that scheduled work remains paused until an operator enables it. This catches missing secrets, local files, undeclared database extensions, and startup ordering assumptions. Backups that nobody has restored are promises, not evidence.

Test the replacement under the same clock and locale assumptions that production uses. Month-end boundaries, daylight-saving changes, database session zones, JVM defaults, and locale-sensitive number parsing can produce results that ordinary daytime replays miss. Drive the clock in tests where the framework permits it, set the deployment time zone explicitly, and include recorded cases from boundary periods. A timestamp that looks equal in HTML can still select a different set of rows or schedule the next run an hour late.

Assign an owner and removal condition to every compatibility layer. Temporary cookie translators, query-shape adapters, proxy normalizers, and dual-schema readers are useful during the move, but they can become permanent undocumented infrastructure. Put each one in the route ledger with the old dependency it protects, the evidence needed to remove it, and the latest stage at which it should remain. When a migrated route no longer needs CFML behavior, delete that bridge before the context disappears. Otherwise the organisation will retire the server and keep paying for its mental model in every future change. Treat bridge removal as part of route acceptance, with the same reviewer and production evidence as the original cutover.

Finally, remove daily dependence on CFML expertise without pretending the history vanished. Keep decision records for intentional parity exceptions and data conversions. Teach the on-call team the new ownership boundaries and failure modes. The goal is that an incident after retirement leads directly to a current service, query, job, or runbook instead of a search for someone who remembers a tag hidden in an include.

Retire ColdFusion only after the hidden work is gone

Zero browser traffic does not mean ColdFusion is unused. Before shutdown, inspect scheduler history, inbound firewall logs, web-server routes, service accounts, database sessions, file shares, mail relays, monitoring checks, backup jobs, and administrator bookmarks. Search for direct hostnames and IP addresses in other repositories and operational runbooks. Keep a deny-and-observe period in which unexpected calls fail visibly or reach a controlled tombstone, rather than disappearing into a powered-off address.

Remove authority in stages. Disable scheduled tasks, revoke datasource writes, stop external connectors, remove the node from the proxy, and watch. Then archive permitted source, configuration, deployment instructions, parity reports, licence records, and final data mappings. Preserve enough to explain historical behavior without preserving a runnable security liability.

The replacement should no longer model CFML concepts where they have no business meaning. Go services can own request and domain behavior, TypeScript can own client interaction, and Postgres can enforce data constraints. Architecture modernisation belongs after parity is measurable, but it must still happen before the old abstractions harden inside the new codebase. Transliteration merely changes the runtime invoice.

A CodeHero ColdFusion rewrite is delivered in under 30 days, with architecture changed rather than tags mechanically copied. The date is useful only because the proof is concrete: route ownership, recorded traffic, database effects, and accepted mismatches all show why the old server can be removed.

The final act is intentionally uneventful. When you stop the ColdFusion service, no user path, job, integration, or operator procedure should notice. If shutdown feels brave, the inventory or parity evidence is incomplete.

FAQ

Will ColdFusion stop working when we do not renew the licence?

Do not assume either outcome. Check the exact release, licence agreement, purchase terms, activation method, and support status, because Adobe has used different licensing models. Test the installed entitlement behavior in an isolated clone while procurement or counsel confirms usage rights.

Should we move from Adobe ColdFusion to Lucee first?

A compatible CFML runtime can reduce immediate licensing pressure, but it does not remove CFML architecture or guarantee behavioral identity. Treat a runtime swap as its own migration with parity tests, not as proof that the application has been modernised.

Can we translate CFML tags directly into TypeScript?

Mechanical translation preserves mixed concerns and misses lifecycle, query, session, and serialization behavior. Define the route contract and data operations first, then implement them behind a proxy boundary. Translate intent, not tag syntax.

What should we migrate first in a ColdFusion application?

Start with inventory and the Application.cfc lifecycle, then choose narrow jobs or read-only endpoints with recordable inputs and outputs. Busy stateful pages come later, after deployment, observability, database adapters, and rollback have already worked in production.

How do we replace ColdFusion query objects?

Do not build a universal imitation unless a short-lived compatibility layer requires it. Return explicit records and test column names, nulls, precision, ordering, generated keys, exceptions, and transaction behavior at the adapter boundary.

Can old and new applications serve traffic at the same time?

Yes, if a proxy assigns clear route or cohort ownership and both sides have compatible session and database behavior. Keep one authoritative writer for each data family. Randomly splitting stateful writes is an invitation to duplicate effects and broken redirects.

How do we test a ColdFusion rewrite for parity?

Replay privacy-safe recorded traffic against the replacement and compare status, headers, normalized body, database changes, and side effects. Review every normalizer and mismatch. Converted line counts and unit tests alone cannot prove that real callers retain their behavior.

Should we redesign the user interface during the migration?

Usually not when licence retirement sets the deadline. A redesign changes workflows and acceptance criteria while the runtime is moving. Preserve the journey first, then redesign after the replacement owns production behavior.

How should we handle ColdFusion sessions during cutover?

Choose deliberately between a compatible shared store, proxy translation, and controlled reauthentication. Test expiry, rotation, serialization, encryption, flash messages, and in-progress forms. Do not let individual routes invent separate session strategies.

When is it safe to shut down the ColdFusion server?

Shutdown is safe after browser routes, jobs, direct integrations, database sessions, file transfers, and operator procedures show no remaining dependency. Revoke authority in stages and observe failures before powering it off. Keep the evidence and configuration needed to explain historical behavior.