Rebuilding a Codebase of Tens of Thousands of Lines Without Breaking It — Large-Scale Refactoring in Practice with Antigravity Editor View
A practitioner's guide to large-scale refactoring with Antigravity's Editor View — dependency analysis, multi-file transformation, staged migration, and the measured cost-benefit of letting AI help.
It happened one morning. I opened one of the sites I run as a solo developer — a codebase I hadn't touched in about three years — and sat there, unable to move, just staring at the screen. Callbacks were nested several layers deep, types were painted over with any, and even I, the person who wrote it, could no longer see what would break if I touched any given file.
The old me would have quietly closed the browser and pretended I hadn't seen it. In a project with thin test coverage, large-scale refactoring makes even the first step feel frightening.
That morning, on a whim, I loaded the entire codebase into Antigravity's Editor View and asked it where I should start. What came back was a list of circular dependencies and a sequence of transformation steps ordered from lowest risk to highest. That was the moment it finally clicked: the fear had never been about the work itself — it was about not being able to see the whole.
This article shares the practical approach to large-scale refactoring with Antigravity Editor View that I picked up over the following stretch, rebuilding several sites and apps (my Dolice Labs projects) in parallel as an indie developer. It comes down to one thing: using AI not as a fully automatic transformer, but as a partner that shows you the whole picture.
This guide is for engineers comfortable with Antigravity's basics who want to take on larger-scale code transformations.
Architecture Analysis — Grasp the Whole Before You Change Anything
The first step in large-scale refactoring is to understand the structure of your current codebase precisely. With Antigravity's Editor View, you can ask the AI to analyze your project with the entire codebase loaded as context.
Generating a Dependency Graph
Open Cmd + I (inline chat) and analyze dependencies with a prompt like this.
Analyze the dependency graph of this project.
Identify circular dependencies, tightly coupled modules,
and suggest the optimal refactoring order based on
dependency depth.
Antigravity uses the Editor View's context window to analyze import/export relationships between files, class inheritance structures, and function call chains. Sample output looks like this.
## Dependency Analysis Report
### Circular Dependencies (Critical)
1. src/services/auth.ts ↔ src/services/user.ts
- auth.ts imports getUserById from user.ts
- user.ts imports validateToken from auth.ts
→ Suggestion: Extract shared interface to src/types/auth-user.ts
### Tightly Coupled Modules (Warning)
1. src/controllers/ → src/services/ → src/repositories/
- 12 controllers directly instantiate service classes
→ Suggestion: Introduce dependency injection container
### Recommended Refactoring Order
1. Extract shared types (lowest risk)
2. Break circular dependencies
3. Introduce DI container
4. Refactor controllers to use DI
You use this analysis to decide the priority and scope of your refactoring. What I personally find most valuable here is that it sorts the work from lowest risk upward. If the first move is just extracting shared types, then even if I change my mind partway through, the damage is almost nil. Being able to start from a reversible step lowers the psychological barrier enormously.
Predicting the Blast Radius
To understand the impact of changing a specific module in advance, this prompt is effective.
If I rename the UserService class and change its constructor
signature, list every file that would need to be updated,
categorized by risk level (high/medium/low).
Beyond static analysis, Editor View factors in the relationship to test files when showing the blast radius. This lets you know what will break before you change anything.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Prompt patterns for grasping the whole picture before you change anything — from dependency graphs to blast-radius prediction
✦How to migrate in stages without taking production down, using the Strangler Fig pattern, feature flags, and a rollback-first design
✦The cost-benefit I measured while refactoring four sites and several apps in parallel as a solo developer — and how to avoid the failures that cost me
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Multi-File Transformation — Where Editor View Shines
Antigravity's Editor View shows its greatest power when handling changes that span multiple files at once.
Pattern-Based Bulk Refactoring
Consider migrating the callback pattern to async/await across an entire project.
// Before: Callback patternfunction fetchUser(id: string, callback: (err: Error | null, user?: User) => void) { db.query(`SELECT * FROM users WHERE id = ?`, [id], (err, rows) => { if (err) return callback(err); callback(null, rows[0] as User); });}// After: Async/Await patternasync function fetchUser(id: string): Promise<User> { const rows = await db.query(`SELECT * FROM users WHERE id = ?`, [id]); return rows[0] as User;}
With the target files selected in Editor View, use this prompt.
Convert all callback-based functions in the selected files
to async/await pattern. Preserve error handling semantics,
update all call sites, and add proper TypeScript return types.
Show me the diff before applying.
The crucial line is "Show me the diff before applying." It makes Antigravity present the changes in Diff View and apply them only after you approve. Even across dozens of files, you can review the diff before applying in bulk, which keeps it safe. Drop that line and the AI rushes straight to rewriting, so I always include it as a fixed phrase.
Incrementally Tightening Types
Migrating from JavaScript to TypeScript, or eliminating the any type, is a refactoring that comes up often in large projects.
// Step 1: AI detects any and infers types// Beforefunction processData(data: any): any { return data.items.map((item: any) => ({ id: item.id, name: item.name.trim(), score: item.score * 100, }));}// After: Result of AI type inferenceinterface DataItem { id: string; name: string; score: number;}interface ProcessedItem { id: string; name: string; score: number;}function processData(data: { items: DataItem[] }): ProcessedItem[] { return data.items.map((item) => ({ id: item.id, name: item.name.trim(), score: item.score * 100, }));}
This inference runs with high accuracy because Antigravity analyzes the calling code and test code.
Migration Strategy for Legacy Code
The best practice for migrating legacy code is to proceed in stages rather than rewriting everything at once. Antigravity's Editor View strongly supports this approach.
Implementing the Strangler Fig Pattern
Implement the Strangler Fig pattern — gradually replacing old code with new — with AI assistance.
// Phase 1: Create an Adapter (bridge old API and new API)class LegacyPaymentAdapter implements PaymentGateway { private legacy: LegacyPaymentService; private modern: ModernPaymentService; private useModern: boolean; constructor(config: { enableModernPayment: boolean }) { this.legacy = new LegacyPaymentService(); this.modern = new ModernPaymentService(); this.useModern = config.enableModernPayment; } async processPayment(amount: number, currency: string): Promise<PaymentResult> { if (this.useModern) { // New path: Modern implementation return this.modern.charge({ amount, currency }); } // Legacy path: Will be removed after migration const result = await this.legacy.oldCharge(amount, currency); return this.normalizeResult(result); } private normalizeResult(legacyResult: LegacyResult): PaymentResult { return { transactionId: legacyResult.tx_id, status: legacyResult.ok ? 'success' : 'failed', timestamp: new Date(legacyResult.ts), }; }}
A sample Editor View prompt looks like this.
Analyze the LegacyPaymentService class and create a
Strangler Fig migration plan:
1. Generate an adapter that wraps the legacy service
2. Define the modern interface based on actual usage patterns
3. Create a feature flag to toggle between legacy and modern
4. List all call sites that need to use the adapter
Automatically Expanding Test Coverage
Raising test coverage before refactoring is the foundation of a safe migration. Combined with AI-powered test generation, you can auto-generate tests for existing code.
Generate comprehensive tests for the LegacyPaymentService
before refactoring. Cover: happy path, edge cases,
error scenarios, and boundary conditions.
Use the existing test patterns in this project.
Antigravity auto-detects the style of existing tests in your project (Jest, Vitest, Mocha, etc.) and generates tests in a consistent style.
Performance Optimization Refactoring
AI-driven performance analysis is another powerful use of Editor View.
Detecting and Fixing N+1 Problems
// Before: N+1 query pattern (auto-detected by AI)async function getOrdersWithProducts(userId: string) { const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [userId]); // ⚠️ N+1: Each iteration triggers a separate query for (const order of orders) { order.products = await db.query( 'SELECT * FROM products WHERE order_id = ?', [order.id] ); } return orders;}// After: Batch query pattern (transformed by AI)async function getOrdersWithProducts(userId: string) { const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [userId]); if (orders.length === 0) return []; const orderIds = orders.map(o => o.id); const products = await db.query( `SELECT * FROM products WHERE order_id IN (${orderIds.map(() => '?').join(',')})`, orderIds ); // Group products by order_id const productsByOrder = products.reduce((acc, p) => { (acc[p.order_id] ||= []).push(p); return acc; }, {} as Record<string, Product[]>); return orders.map(order => ({ ...order, products: productsByOrder[order.id] || [], }));}
You can detect and fix patterns like these in one pass with this prompt.
Scan the repository for N+1 query patterns, unnecessary
re-renders in React components, and synchronous file
operations that could be parallelized. For each finding,
generate the optimized version with before/after comparison.
The Cost-Benefit I Measured — Rebuilding Several Projects in Parallel
From here, I'll share — in terms as close to numbers as I can — what I felt while refactoring with Editor View as an indie developer running four Dolice Labs sites and several mobile apps in parallel. It's one person's operational record, but it should serve as a rough yardstick.
The "three years of technical debt" codebase I mentioned at the start had any in over 200 places and seven sets of circular dependencies. At that scale, the impact investigation alone would have eaten several days by hand. When I handed dependency analysis to Editor View, I understood the structure in half a day. What shrank the most wasn't "writing time" — it was "investigation time."
In practice, the work where AI helped a lot and the work where it didn't split cleanly. The table below organizes the tendencies I felt repeatedly while running everything in parallel.
Type of work
Benefit of delegating to AI
Judgment a human must keep
Dependency / blast-radius investigation
Very high (days → half a day)
Where to draw module boundaries
Mechanical typing / pattern conversion
High (you focus on the diff)
Naming and public interface design
Generating test scaffolds
Moderate (humans fill coverage gaps)
Defining "what must not break"
Architectural direction
Low (proposals are reference only)
Almost entirely the human's
What I learned from this table is that AI shows its true value in the repetitive "investigate and replace" work, while design judgments — where to draw the boundary, what must never break — remain a human's job to the very end. If you want to maximize the cost-benefit, the shortcut is to narrow what you hand the AI down to "reversible, mechanical transformations."
Three Moments AI Nearly Burned Me
Honestly, it didn't go smoothly from the start. While running everything in parallel, there were several moments that made my stomach drop. I'll share three I keep on record to prevent recurrence.
The first was bulk-transforming areas with thin test coverage. The diff looked clean, but because the functions had no tests, I only noticed the behavioral difference in production. Since then, I always check coverage before transforming, and I insert a "write the tests first" step for any area without them.
The second was the AI tidying up surrounding code "while it was at it." When it changes variable names or formatting beyond the requested scope, review noise spikes instantly. I now add "Only modify what is necessary for this change. Do not reformat unrelated code." to the prompt to keep changes minimal.
The third was putting in too much at once, so I couldn't isolate which change caused a problem. When you apply dozens of files at once and tests fail, identifying the cause takes longer, not shorter. Now I rigorously follow "one concern per commit" and slice things small, module by module.
None of these are extraordinary failures. But turning each small ache into a step in the procedure is the patient road to growing AI into a partner you can trust.
Designing for Rollback — Feature Flags and a Kill Switch
When I push a large change to production, what I value most is keeping an "I can always go back" state. I believe the courage to move forward comes precisely from having a way out.
When you wrap the Strangler Fig pattern with a feature flag, you can switch between old and new implementations with a single environment variable. On top of that, I build in a "kill switch" from the very beginning that instantly reverts to the old implementation when an anomaly is detected.
// Disable the new implementation instantly via env var + runtime monitoringfunction shouldUseModern(): boolean { if (process.env.MODERN_PAYMENT_KILL_SWITCH === 'on') { return false; // In emergencies, fall back to legacy, no questions asked } return process.env.ENABLE_MODERN_PAYMENT === 'true';}
Just having this switch completely changes the psychological weight of refactoring. Knowing that "even if it fails, I can revert with a one-line environment variable" lets me move forward boldly. When I have Editor View draft a migration plan, I always add "Include a kill switch and rollback path." to the requirements.
A Workflow for Safe Refactoring
Finally, here is the workflow I settled into for carrying out large-scale refactoring without failure.
Step 1: Analysis Phase
In Editor View, include the entire project as context and run an architecture analysis. Identify the dependency graph, circular references, and tightly coupled modules.
Step 2: Test Reinforcement Phase
For the modules you'll change, raise coverage above 80% with AI test generation. Focus especially on boundary-value tests and error-handling tests.
Step 3: Staged Transformation Phase
Apply transformations module by module while reviewing diffs in Diff View. Run tests after each step to confirm existing functionality isn't broken.
# Run after each transformation stepnpm test -- --coverage --changedSince=main
Step 4: Integration Testing Phase
Once all transformations are complete, run end-to-end tests to confirm there are no integration issues.
# Run E2E testsnpm run test:e2e# Type checknpx tsc --noEmit# Lintnpm run lint
Closing — Your Next Step
Large-scale refactoring with Antigravity's Editor View rests on three pillars: AI-driven architecture analysis, multi-file transformation, and staged migration support. Refactoring an entire codebase, which used to take weeks, can now be completed safely in a matter of days with AI's help.
But what struck me most through all of this was a matter of attitude more than technology. Welcoming AI not as a fully automatic tool but as a partner that shows you the whole picture. Reviewing in Diff View, starting from a reversible step, wiring in a kill switch first — making those three a habit stabilizes the success rate of large-scale refactoring to a surprising degree.
If you're holding a codebase you're afraid to open right now, start by throwing it a single dependency-analysis prompt. The moment the whole comes into view, the fear turns into a to-do list. I hope your project gets quietly and surely rebuilt.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.