From Spaghetti to SOLID: AI Prompts That Turn Legacy Code into Clean Architecture

Legacy code. Just the phrase can make a developer's shoulders tense. It's the codebase that works well enough to be profitable, but is so tangled that every new feature requires a week of archaeology. You dream of a clean, modular, testable system, but the risk of breaking everything in a rewrite is too high. The good news: you don't need a big-bang rewrite. You can refactor incrementally, and AI can be your partner in this process. With the right prompts, you can analyze dependencies, extract modules, migrate to modern patterns, and boost test coverage safely. This guide provides a set of practical prompts to help you turn technical debt into a clean architecture, one step at a time.

1. Dependency Analysis: Map the Monster

What it's for: Before you can refactor, you need to understand the current state. This prompt helps you identify tight coupling, circular dependencies, and hidden relationships between classes and modules.

Prompt:
"Act as a senior software architect. Analyze the [language] codebase in the current workspace. Focus on the following:
1. Identify all modules/packages and their dependencies.
2. Detect circular dependencies and report them as critical issues.
3. For each module, list its incoming and outgoing dependencies, highlighting those that violate the Dependency Inversion Principle (e.g., high-level modules depending on low-level implementations).
4. Suggest an ideal dependency graph that would align with Clean Architecture principles.
Provide a structured report with file paths, dependency types, and concrete recommendations. Use PlantUML or Mermaid to visualize the current and proposed dependency graphs."

Example use: In a Java Spring project with a tangled service layer, this prompt can quickly reveal that OrderService directly instantiates EmailSender and InventoryClient (concrete classes), instead of depending on interfaces. The AI will flag this and suggest introducing interfaces and using dependency injection.

2. Extract God Class: Break It Down

What it's for: God classes are the classic legacy anti-pattern: one class that does everything. This prompt helps you decompose it into smaller, single-responsibility classes.

Prompt:
"Refactor the class [ClassName] in [file path]. This class currently handles [list of responsibilities]. Apply the Single Responsibility Principle to split it into multiple classes. For each new class:
- Name it descriptively.
- List the exact methods and fields to move.
- Specify the public interface of the new class.
- Show how the original class can delegate to the new classes (composition).
Ensure backward compatibility: the original class should still compile and pass existing tests."

Example use: A UserManager class that handles authentication, profile updates, and password reset. The prompt will split it into UserAuthenticator, UserProfileService, and PasswordService, with UserManager acting as a facade. The AI can generate the skeleton code for each new class.

3. Replace Magic Numbers and Strings with Constants

What it's for: Magic numbers/strings make code hard to maintain. This prompt helps you find and replace them with named constants/enums.

Prompt:
"In the codebase, find all magic numbers and strings (e.g., hardcoded timeouts, error codes, status values) that are used without explanation. For each, suggest a descriptive constant name and the appropriate place to define it (e.g., a constants class, enum, or config file). Provide a list of replacements in the form of a table: file, line, current value, suggested constant. Do not change behavior; just improve readability."

Example use: In a Python script, if response.status_code == 200: becomes if response.status_code == HTTPStatus.OK:. The AI will list all such occurrences and the new constant references.

4. Add Type Hints and Explicit Interfaces

What it's for: Dynamic languages (JavaScript, Python) often lack type information, which hides bugs. This prompt helps add type hints and document the interface of functions.

Prompt:
"For the file [file path], add type hints to all function signatures and variables where possible. For classes, define explicit interfaces (e.g., using Python's typing.Protocol or TypeScript's interface). If a function returns different types, use union types or generics. Also add docstrings/comments explaining the expected types and return values. Preserve the runtime behavior exactly."

Example use: In a Python function def calculate_discount(price, user):, the AI will change it to def calculate_discount(price: float, user: User) -> float: and add a docstring explaining the business logic.

5. Migrate to Modern Patterns (Strategy, Factory, Observer)

What it's for: Legacy code often has long if-else chains or switch statements. This prompt helps replace them with design patterns.

Prompt:
"Refactor the code in [file path] that uses conditional logic (if-else or switch) to select an algorithm or behavior. Apply the Strategy pattern: define an interface for the algorithm, create concrete strategy classes for each branch, and use a factory or a map to select the strategy at runtime. Show the code for all new classes and the modified client code. Keep the public API unchanged."

Example use: In a payment processing function with if (type == 'credit') ... else if (type == 'paypal') ..., the AI will create PaymentStrategy interface, CreditCardPayment, PayPalPayment classes, and a PaymentFactory.

6. Write Characterization Tests for Existing Behavior

What it's for: Before refactoring, you need a safety net. Characterization tests capture current behavior, so you can refactor with confidence.

Prompt:
"Write characterization tests for the class [ClassName] in [file path]. The goal is to lock in the current behavior, not to fix it. Create a test file that:
- For each public method, write a test that exercises the method with typical, edge, and error inputs.
- Assert the exact current output (including exceptions and error messages).
- Use a test framework like JUnit, pytest, or Jest.
- Name tests descriptively, e.g., 'test_fetch_returns_200_when_valid_id'.
Provide the complete test code."

Example use: For a LegacyPaymentProcessor, the AI generates tests that call processPayment(amount, method) with various inputs and assert the exact return values or thrown exceptions. These tests become your regression suite during refactoring.

7. Refactor Monolithic Functions into Smaller Units

What it's for: Large functions (over 50 lines) are hard to test and understand. This prompt helps you break them down.

Prompt:
"The function [functionName] in [file path] is too long (over N lines). Break it down into smaller, cohesive helper functions using the Extract Function refactoring technique. For each extracted function:
- Give it a descriptive name.
- Define its parameters and return value.
- Explain the logic it encapsulates.
- Show the refactored code of the original function using the new helpers.
Ensure the behavior is identical."

Example use: A 200-line processOrder function is split into validateOrder, calculateTotals, applyTaxes, and persistOrder. The AI provides the full refactored function.

8. Eliminate Code Duplication (DRY)

What it's for: Copy-paste is rampant in legacy code. This prompt helps identify and remove duplication.

Prompt:
"Analyze the codebase for duplicated code blocks (exact or near-exact copies). For each instance, propose a refactoring that eliminates the duplication, such as extracting a common function, using inheritance, or creating a utility class. Provide a list of files and lines involved, and show the new code for each refactoring."

Example use: In a PHP project, the AI finds that formatDate is defined in three different files with slight variations. It suggests a single DateHelper class and shows how to update the calling code.

9. Improve Error Handling and Logging

What it's for: Legacy code often has poor error handling (swallowed exceptions, empty catch blocks) and no logging. This prompt helps improve observability.

Prompt:
"Review the error handling in [file path]. Identify:
- Empty catch blocks or exceptions that are silently swallowed.
- Missing error handling for I/O or network operations.
- Lack of logging for critical operations.
For each issue, provide a fix: add proper try-catch blocks, log the exception with context (e.g., using a logger), and rethrow if necessary. Also add structured logging (e.g., JSON format) for important events."

Example use: A Java method that catches Exception and does nothing is fixed to log the error and wrap it in a custom exception, providing more context.

10. Database Migration: Refactor SQL Queries to a Data Access Layer

What it's for: Embedded SQL in business logic is hard to maintain and test. This prompt helps move it to a data access layer.

Prompt:
"The file [file path] contains SQL queries directly in business logic. Refactor by creating a data access layer (DAO/Repository) that encapsulates these queries. For each query:
- Create a method in the repository class with a descriptive name.
- Use parameterized queries to prevent SQL injection.
- Return strongly-typed objects where possible.
- Update the business logic to use the repository instead of direct SQL.
Show all new code."

Example use: In a C# app, the AI extracts SELECT * FROM Orders WHERE CustomerId = @id into OrderRepository.GetOrdersByCustomer(int customerId).

11. Introduce Dependency Injection

What it's for: Legacy code often creates dependencies inside classes (new keyword), making testing difficult. This prompt helps introduce DI.

Prompt:
"Refactor the class [ClassName] to use Dependency Injection. Identify all dependencies that are instantiated inside the class (e.g., new Service()). Change the constructor to accept these dependencies as parameters, and store them as private fields. If the class is used in many places, provide examples of how to update the call sites. Also show how to register the dependencies in a DI container (e.g., Spring, .NET Core, or simple manual DI)."

Example use: A ReportGenerator that creates a DatabaseConnection becomes one that receives an IDatabaseConnection in its constructor. The AI provides code for the DI container configuration.

12. Performance Optimization: Find Bottlenecks

What it's for: Legacy code often has performance issues (N+1 queries, loops in the wrong place). This prompt helps identify and fix them.

Prompt:
"Analyze the code in [file path] for common performance bottlenecks:
- N+1 queries in loops.
- Unnecessary object creation in loops.
- Inefficient data structures (e.g., using list for membership checks).
- Missing caching for expensive operations.
For each issue, provide a specific fix (e.g., use joins, use a set, add caching). Show the before and after code."

Example use: In a Node.js API, the AI spots that a user list endpoint fetches user details in a loop, resulting in 100+ DB queries. It suggests using a single query with IN clause.

Final Thoughts

Refactoring legacy code is a marathon, not a sprint. These prompts give you a systematic approach to reduce technical debt while keeping your system stable. Start with dependency analysis and characterization tests, then gradually apply the other techniques. The key is to make small, safe changes and let AI handle the heavy lifting. Remember, the goal is not perfect code, but better code that is easier to maintain and extend. Use these prompts as a starting point, and adapt them to your specific stack and needs. Happy refactoring!

← All posts

Comments