The demo works. Users have started arriving. Then a pricing change breaks bookings, a permissions fix touches three screens, and nobody knows why two services calculate the same amount differently. You have just inherited the repository.
For a developer or CTO, taking over this codebase starts with one question: can we predict the consequences of the next change? The answer depends on the business rules, the boundaries in the code, and the available ways to verify behavior.
Our article on vibe coding’s benefits and limits helps you decide when to use it. The application takeover guide covers auditing and production readiness. Here, we go inside the repository: tests, vocabulary, modules, architectural decisions, and migrations.
The risk: code the team can no longer explain
Generated code can be well designed, reviewed, and tested. An application written entirely by hand can accumulate the same problems. The origin of the code tells you little about its ability to evolve; examine its behavior and the decisions behind its structure.
The 2025 DORA report describes AI as an amplifier of an organization’s strengths and weaknesses. That finding does not assess your repository. Our takeaway for a recovery project is to improve understanding and validation before accelerating code production again.
One symptom deserves particular attention: nobody can explain a rule without tracing the entire application or asking a model to reconstruct it. A plausible explanation remains a hypothesis until executed code, tests, and domain experts confirm it.
Before refactoring: establish a reference version
Start with a reproducible environment, an identified code version, locked dependencies, and test data. Verify that the team can deploy and restore the service, then trace a critical journey from its screen through storage and external services.
If you discover exposed data, unauthorized access, or compromised secrets, address that risk before architectural cleanup. Reorganizing folders cannot make an unsafe application safe.
Next, choose an initial scope: a business capability that changes frequently, causes incidents, or blocks a release. The following five areas can be addressed within that scope before expanding to the rest of the application.
1. Improve testability before changing the rules
Michael Feathers’ characterization tests capture what a program actually does. They help detect behavioral changes during a takeover, even when the original specification is missing. They do not prove that the behavior is desirable.
Consider a fictional booking application, which we will use throughout this article. Before reorganizing its code, observe a confirmed booking, a cancellation, and a repeated request after a network interruption. Record responses and effects: booking state, database writes, and notifications sent.
Separately, validate expected outcomes with domain experts. If the current behavior allows two confirmations for the last available place, preserve a reproduction of the defect and write a test for the corrected outcome. A known bug must not become a requirement merely to preserve existing behavior.
Testability improves when dependencies can be controlled. Separate calculating a deadline from reading the clock, and the decision to confirm from calling a payment provider. Test rules without a network; also verify the actual adapters and database constraints in integration tests.
- Business rules: expiration, cancellation, partial refunds, and pricing changes.
- Integrations: concurrent requests for the last place, delayed responses, and duplicate events.
- Critical journeys: confirmation shown to the correct user, and access denied from another organization.
For the CTO, the first expected outcome is a useful change delivered under the protection of these tests. Overall coverage can provide additional information; it cannot replace checks on the product’s actual risks.
2. Establish a shared domain language
An application may call the same thing a “booking” in the interface, an “order” in the API, and a “session” in the database. It may also use “customer” to mean a signed-in person, an organization, and a billing account. That ambiguity eventually affects permissions and business rules.
Ubiquitous Language, explained by Martin Fowler from Eric Evans’ Domain-Driven Design, brings developers’ vocabulary together with that of domain experts. It evolves as the team’s understanding grows.
For our fictional booking system, an initial workshop might produce this:
| Term | Agreed meaning | Rule to verify |
|---|---|---|
| Hold | A temporarily reserved place | Has an explicit expiry time |
| Booking | A confirmed reservation | Respects available capacity |
| Payment | A payment operation | Its state is distinct from booking state |
| Organization | A team’s access boundary | Cannot read another organization’s bookings |
These definitions need validation for the specific product. Does a failed payment cancel the booking or start a grace period? Code alone cannot decide what the business wants to happen.
Carry the agreed terms into types, operations, tests, and domain documentation. Keep explicit translations for older APIs. Two domains may legitimately use different words: forcing them together would introduce another ambiguity.
A coding agent should receive these definitions and their invariants before proposing a change. They also give reviewers concrete criteria for accepting or rejecting the proposal.
3. Build deep modules with simple contracts
A deep module provides substantial functionality through a relatively simple interface. In his discussion with Robert C. Martin, John Ousterhout explains why multiplying small functions can scatter complexity instead of reducing it.
In our example, each screen might currently check availability, calculate a price, create a booking, and send a notification. Adding helper files still leaves every caller responsible for coordinating those steps correctly.
A business operation such as confirmBooking can expose a more useful contract: authenticated identity, the relevant booking, an idempotency key, and possible outcomes. The module coordinates confirmation rules; callers can also distinguish an unavailable place, denied access, and a pending payment.
A short interface is insufficient on its own. Its contract must describe effects and failures. A local transaction cannot make an external provider call atomic: intermediate state, failure recovery, and duplicate prevention must remain explicit.
Look for coherent responsibilities: booking, billing, and identity. They can live in the same deployment. Extracting microservices would add network and operational constraints that need a separate justification.
Acceptance criterion: changing a confirmation rule changes the module and its tests; callers remain stable while the contract stays the same. File count and file length provide little information without checking this behavior.
4. Explain surprising decisions with ADRs
Code shows how a solution works, but sometimes leaves its motivation invisible. Why keep two identifiers? Why process an event asynchronously? Why temporarily accept two formats?
An Architecture Decision Record, or ADR, preserves a significant decision, its context, status, and consequences. Michael Nygard proposes short documents versioned with the code, retaining earlier decisions when they are superseded.
An ADR for our fictional example might contain the following:
- Context: the provider can deliver the same payment event more than once.
- Decision: record the event identifier and make its processing idempotent.
- Rejected option: assume every delivery represents a new payment.
- Consequences: define database uniqueness, failure recovery, and tests for concurrent deliveries.
- Status: accepted, with links to the corresponding implementation and tests.
During a takeover, an agent can help locate evidence of a decision. If the history is missing, record that the rationale still needs confirmation. A justification invented today is not the project’s memory.
Reserve ADRs for consequential choices. A local condition may only need a clearer name or a comment explaining its constraint. Useful documentation prevents a future change from removing a protection whose purpose was misunderstood.
5. Automate repetitive migrations
Once the target contract is validated, dozens of call sites may need updating. That is a good candidate for a codemod: a code transformation you can inspect, test, and rerun.
jscodeshift provides tools for transforming multiple JavaScript or TypeScript files through their syntax structure. This lets a transformation target code patterns instead of replacing text indiscriminately.
In our example, a transformation could replace calls to the old booking service whose parameters match the new contract. Ambiguous cases should be left for manual review. A syntax tree alone cannot prove that two operations have the same business meaning.
- Test the transformation against before-and-after examples, including files it must ignore.
- Review a dry run on a small scope, checking imports, aliases, and signatures.
- Apply changes in batches, then run type checking, tests, and diff review.
- Verify that a second run leaves the result unchanged and identify remaining old calls.
A code migration does not automatically migrate data. For incompatible API or schema changes, Danilo Sato’s Parallel Change pattern separates three phases: expand, migrate, and contract.
Introduce a new format that supports the transition, migrate consumers and data, then verify that no old readers or writers remain. Remove the old format afterward. If both systems write concurrently, also define how to detect and reconcile divergence.
Rollback must account for transformed data and external effects. Returning to the previous commit is insufficient after dropping a column or sending a notification.
What the CTO needs to decide
Make the decision per business capability. A usable interface may be retained while the permissions module needs replacement. Martin Fowler’s Strangler Fig approach provides a framework for replacing a system incrementally and assessing results throughout the transition.
| Observed condition | Possible decision | Evidence required |
|---|---|---|
| Behavior is understood, stable, and verified | Keep | The next change remains local and testable |
| Reliable rules with tightly coupled dependencies | Refactor incrementally | Behavioral tests remain valid |
| Fragile subsystem with an identifiable boundary | Isolate, then replace | Verified contract and migration strategy |
| Model conflicts with requirements and repair is too costly | Evaluate rewriting the affected scope | Comparison includes data, transition, and operations |
Before funding a full recovery project, choose a bounded pilot: for example, make booking confirmation reliable and deliver a real change to its rules. Estimate understanding, risk reduction, refactoring, and migration separately. Record unknowns and set a point to reassess the approach.
Track the time needed to deliver that change, regressions, review effort, and the team’s ability to diagnose failures. A repository with more tests and documents that remains difficult to change has not yet reached the goal.
Continue using AI after the recovery
Agents remain useful for mapping dependencies, proposing edge cases, and preparing repetitive transformations. Ask for precise file references, explicit assumptions, and changes small enough to review.
Expected test outcomes must come from validated rules or observed behavior that the team understands. Generating both an implementation and its evaluation from the same assumption can simply reproduce an error twice.
The first goal of recovery is concrete: another person on the team can explain, change, test, and deploy a business capability without rediscovering the entire application. All five areas support that autonomy.
To prepare a takeover with Appik Studio, show us the journey that is blocking you and the next change you need. We can scope the analysis around that change, the associated risks, and the parts of the product worth retaining.
Sources and references
- DORA — State of AI-assisted Software Development 2025
- Michael Feathers — Characterization Testing
- Martin Fowler — Ubiquitous Language
- John Ousterhout and Robert C. Martin — module decomposition and complexity
- Michael Nygard — Documenting Architecture Decisions
- jscodeshift — JavaScript and TypeScript code transformations
- Danilo Sato — Parallel Change, expand / migrate / contract
- Martin Fowler — incremental modernization with Strangler Fig
