Brownfield software development means changing an existing application with existing data, users and dependencies. The starting point includes behavior people already rely on, undocumented workarounds and a deployment process that may differ from the README.
A useful takeover produces an evidence inventory, a repeatable setup, a baseline for important behavior and a bounded first change. This guide uses a fictional distributor's order-approval portal: an approved order revision is exported to fulfillment. The small PHP example below was exercised in isolation; it is not a client system or a production migration.
Use the legacy modernization workbook (Markdown) to record findings and unknowns. The broader legacy system modernization guide covers the decision to retain, improve or replace the application.
1. Inventory the application and the access available
Identify the business owner, technical owner and people who operate the workflow. Agree who may inspect code, logs and sanitized records, and who may change configuration or deploy. Record credential owners and access references without copying secret values into the inventory.
AWS's discovery guidance includes application owners, infrastructure, databases, dependencies and usage evidence. For a small application, a short register can capture those facts without adopting a large migration programme.
| Area | Evidence to collect | Unknown to flag |
|---|---|---|
| Runtime and dependencies | Deployed versions, lockfiles, extensions and patch owner. | Repository configuration differs from the running host. |
| Business workflow | Approval states, revision identifiers, export samples and operator steps. | A manual correction has no documented rule. |
| Jobs and integrations | Schedules, queues, file destinations, acknowledgements and failure handling. | A consumer or retry process has no known owner. |
| Release and recovery | Deployed revision, deployment procedure, backup location and restore evidence. | A successful restore has not been demonstrated. |
Add the source, observation date and confidence to each entry. “Owner says this job is unused” and “disabled in the inspected scheduler” are different observations. An inaccessible component remains an explicit gap.
Check runtime support against the responsible vendor, such as PHP's supported-version table. Record framework, database and operating-system support separately. An upstream branch's support status does not establish that the deployed point release or every dependency is patched.
2. Make the setup reproducible before changing behavior
Start from a recorded revision and its lockfiles. Document required runtime versions, extensions, environment-variable names, database setup and asset-build steps. Use test configuration and synthetic records that preserve the relationships needed for the workflow.
Run the existing checks and record failures before editing. Separate application failures from missing services or an incomplete fixture. A replacement mail driver can help isolate a test; it does not demonstrate that production mail delivery works.
Have another developer follow the setup notes without relying on the first developer's shell history. Record anything that still requires manual preparation. This gives the first change a reproducible starting point and gives the next maintainer a usable handover artifact.
3. Separate observed behavior from desired behavior
Trace one important workflow through its actual boundaries. In the fictional portal, follow an order revision from approval to export, file delivery and fulfillment acknowledgement. Include scheduled jobs and operator actions that happen after the web request finishes.
Keep two records: what the application currently does, and what the business owner says it should do. A mismatch needs a decision. A characterization check captures an observed result so a later change can expose a difference; it does not make that result correct or authorize preserving a known defect indefinitely.
For the CSV boundary, record the column sequence, which revisions are eligible, row ordering, decimal formatting and line endings. Ask the consuming system's owner which details are contractual. A file that looks equivalent in a spreadsheet can still contain different bytes.
4. Put a small characterization check around the boundary
This synthetic exporter receives normalized tuples: order ID, current revision, approved revision, status and a decimal total string. It exports only matching approved revisions, preserving the supplied row order. Authentication, record selection for a particular user and input validation are outside this function.
Save this example as export-approved-orders.php:
<?php
function exportApprovedOrders(array $orders): string
{
$out = fopen('php://temp', 'w+');
if ($out === false) {
throw new RuntimeException('Cannot open CSV stream.');
}
try {
$rows = [['order_id', 'revision', 'total']];
foreach ($orders as [$id, $revision, $approvedRevision, $status, $total]) {
if ($status === 'approved' && $revision === $approvedRevision) {
$rows[] = [$id, (string) $revision, $total];
}
}
foreach ($rows as $row) {
if (fputcsv($out, $row, ',', '"', '', "\r\n") === false) {
throw new RuntimeException('CSV write failed.');
}
}
rewind($out);
$csv = stream_get_contents($out);
if ($csv === false) {
throw new RuntimeException('CSV read failed.');
}
return $csv;
} finally {
fclose($out);
}
}
The PHP manual documents fputcsv's delimiter, enclosure, escape and line-ending options. The example supplies an empty escape parameter explicitly and uses CRLF. It carries the decimal strings through unchanged rather than converting them to floating-point numbers.
Save the following beside it as check-export.php. The third row deliberately has a stale approved status: revision 3 is current, but approval belongs to revision 2. The revision comparison must still exclude that row.
<?php
require __DIR__.'/export-approved-orders.php';
// ID, current revision, approved revision, status, decimal total string.
$orders = [
['SO-102', 2, 2, 'approved', '120.00'],
['SO-104', 1, null, 'draft', '45.00'],
['SO-103', 3, 2, 'approved', '85.50'], // Deliberately stale approval flag.
['SO-101', 1, 1, 'approved', '9.50'],
];
$expected = "order_id,revision,total\r\n"
."SO-102,2,120.00\r\n"
."SO-101,1,9.50\r\n";
if (exportApprovedOrders($orders) !== $expected) {
throw new RuntimeException('CSV boundary changed.');
}
echo "CSV boundary matched.\n";
Run the check in the directory containing those two files:
php check-export.php
We ran these exact snippets with PHP 8.3.6. The 57-byte expected output matched, including the header, the two eligible rows in supplied order, decimal strings and CRLF endings. We then removed the revision comparison from the export condition in an isolated copy. The same check failed with CSV boundary changed. because the stale-approved revision was no longer excluded.
That is a useful boundary check, with limited scope. It does not establish user authorization, the correctness of approval creation, CSV handling of arbitrary untrusted input or throughput on a large export. The tiny fixture is evidence about its specified input and output, not a complete production exporter.
5. Choose a first change with an inspectable result
Use the findings to select one improvement with a clear reason and acceptance boundary. In the portal, that might be moving CSV formatting out of a controller while preserving the characterized output. A separate defect fix might change which revision is exportable; that needs an explicitly revised expectation and business agreement.
Write down the files and workflow affected, the behavior to preserve, the intended difference and the checks required. Keep an unrelated framework upgrade, authentication redesign or storage migration out of that change. Each additional moving part makes an unexpected result harder to explain.
If the first investigation discovers a wider dependency, update the estimate and proposed next step. A bounded assessment should produce enough evidence to choose the next commitment, including a clear record of what could not yet be established.
6. Prepare the release and its recovery boundary
Connect the reviewed revision to the artifact or code that will be released. Run the boundary check alongside applicable application and integration checks. Our Laravel CI/CD guide shows why application tests, server behavior and release selection need separate evidence.
Rehearse the release in a suitable lower environment. Identify configuration, schema, cache and worker steps that actually apply. For the fictional export, inspect both the produced file and the downstream acknowledgement; a successful HTTP response alone does not show that fulfillment accepted it.
Define who can stop the release, which observation triggers recovery and what state will exist then. AWS's cutover guidance distinguishes returning before new transactions from recovery after the target has accepted writes. Record a separate data strategy when the old system may be stale.
In this example, reverting code would not recall an order file already accepted by fulfillment. The operating procedure must say how to reconcile or correct that external action. Treat that as an owned business process, rather than assuming a code rollback handles it.
7. Leave a handover that explains the remaining work
The takeover should leave a short, reviewable set of artifacts:
- Inventory: components, owners, dependencies, evidence dates and access gaps.
- Setup and baseline: revision, environment, reproducible commands and existing failures.
- Behavior record: representative fixtures, current output and agreed intended changes.
- First-change record: scope, review evidence, release steps and recovery limits.
- Follow-up queue: prioritized risks and improvements, with the evidence needed for each decision.
Keep unresolved items visible: an untested restore, an unknown export consumer or an unexplained manual adjustment is useful information for the next maintainer. Do not label the whole application verified because one boundary now has a regression check.
The legacy maintenance cost guide helps compare ongoing work and proposed improvements. For a Laravel takeover, our Laravel development services discussion starts with the existing workflow, the available evidence and a first change that can be reviewed independently.
