DEV Community

Samcorp
Samcorp

Posted on

We Migrated 400k Lines From AngularJS - What It Actually Took

We Migrated 400k Lines From AngularJS - What It Actually Took

At first, 400,000 lines of AngularJS sounded like the problem.

It wasn't.

The harder part was figuring out what those lines actually represented.

Some were straightforward controllers and templates. Others contained years of business rules, shared services, custom directives, routing assumptions, and dependencies nobody wanted to touch.

That changed how we approached the entire AngularJS migration.

Instead of thinking:

400,000 legacy lines
        ↓
Rewrite everything
        ↓
Done
Enter fullscreen mode Exit fullscreen mode

we moved toward:

Understand
   ↓
Create boundaries
   ↓
Migrate incrementally
   ↓
Validate behavior
   ↓
Remove legacy code
Enter fullscreen mode Exit fullscreen mode

That made a huge codebase feel much more manageable.


The First Mistake: Estimating by Lines of Code

Line count tells you the size of a codebase.

It doesn't tell you how difficult that codebase will be to migrate.

Consider two 500-line files:

File A
500 lines
Simple CRUD screen
Enter fullscreen mode Exit fullscreen mode

and:

File B
500 lines
Authentication
Permissions
Shared state
Pricing rules
Five integrations
Enter fullscreen mode Exit fullscreen mode

They have the same line count.

They are completely different migration tasks.

Instead of estimating only by LOC, we started mapping:

  • Routes
  • Modules
  • Controllers
  • Components
  • Directives
  • Services
  • Filters
  • API clients
  • Shared state
  • Third-party libraries
  • Tests
  • Styles
  • Build tooling

The better question became:

What depends on this feature, and what does this feature depend on?

That exposed the real migration surface.


1. Inventory the Application Before Rewriting It

It was tempting to start a fresh Angular application immediately.

Instead, we first mapped the existing system.

Application
│
├── Authentication
├── Dashboard
├── Customers
│   ├── Customer List
│   ├── Customer Detail
│   └── Customer Search
├── Orders
│   ├── Order Entry
│   ├── Pricing
│   └── Approval
├── Reporting
└── Admin
Enter fullscreen mode Exit fullscreen mode

Then we added dependency relationships.

For example:

Order Entry
    ↓
Customer Service
    ↓
Pricing Service
    ↓
Permissions
    ↓
Shared API Client
Enter fullscreen mode Exit fullscreen mode

This exposed something important.

We didn't actually have one 400k-line problem.

We had a graph of smaller problems.

For teams planning an AngularJS migration to modern Angular, the broader Angular ecosystem and implementation considerations are also covered by SDLC Corp's Angular Development Company page.

The useful part wasn't choosing a new framework.

It was understanding the old application well enough to decide what should move first.


2. We Avoided a Big-Bang Rewrite

One of our most important decisions was allowing AngularJS and modern Angular to coexist temporarily.

For a large application that was still actively changing, freezing development and rewriting everything behind closed doors would have created too much risk.

Instead, the migration looked closer to:

AngularJS application
        ↓
Introduce Angular
        ↓
Migrate one boundary
        ↓
Validate
        ↓
Migrate another boundary
        ↓
Shrink AngularJS
        ↓
Eventually remove AngularJS
Enter fullscreen mode Exit fullscreen mode

This was effectively a strangler-style modernization strategy.

And it changed the migration from:

"When will the rewrite finally be finished?"

to:

"Which legacy boundary can we safely remove next?"

That second question was much easier to answer.


3. We Migrated Features, Not Random Files

Early on, it was tempting to convert whichever AngularJS file looked easiest.

That quickly creates a confusing architecture:

Angular controller
AngularJS template
Angular service
AngularJS directive
Angular component
Enter fullscreen mode Exit fullscreen mode

Technically, code has moved.

Architecturally, nobody knows who owns the feature.

We had better results migrating vertical slices.

For example:

Customer Search
│
├── UI
├── Validation
├── API calls
├── State
└── Tests
Enter fullscreen mode Exit fullscreen mode

Once that slice moved, ownership was clear:

Before:
AngularJS owns Customer Search

After:
Angular owns Customer Search
Enter fullscreen mode Exit fullscreen mode

This made migration progress understandable to developers, testers, and product owners.


4. Shared Services Were Harder Than Screens

The UI wasn't always the difficult part.

Shared services were.

A service might look innocent:

app.service('customerService', function($http, cacheService) {
    // business logic accumulated over several years
});
Enter fullscreen mode Exit fullscreen mode

But it could be used by:

Customer screens
Orders
Billing
Reporting
Search
Admin
Enter fullscreen mode Exit fullscreen mode

Migrating one screen didn't justify rewriting its entire dependency graph.

So we introduced clear service boundaries.

Conceptually:

AngularJS component
       ↓
Shared service boundary
       ↑
Angular component
Enter fullscreen mode Exit fullscreen mode

That let us migrate consumers gradually rather than forcing related systems to move together.


5. We Separated Framework Code From Business Logic

Old AngularJS applications often contain business logic directly inside controllers.

For example:

$scope.approveOrder = function(order) {
    // validation
    // permission checks
    // pricing rules
    // API call
    // notifications
    // navigation
};
Enter fullscreen mode Exit fullscreen mode

Simply translating that into an Angular component would preserve the underlying architectural problem.

Instead, we tried to move toward:

Component
    ↓
Application service
    ↓
Business rules
    ↓
API
Enter fullscreen mode Exit fullscreen mode

The component handles UI coordination.

Business rules live somewhere testable and reusable.

That meant the migration became an opportunity to improve architecture instead of performing syntax conversion.


6. Templates Took Longer Than Expected

Changing:

<div ng-if="customer.active">
Enter fullscreen mode Exit fullscreen mode

to its modern equivalent looks trivial.

Real templates weren't that simple.

They combined:

ng-if
ng-show
ng-hide
ng-repeat
ng-class
ng-click
filters
custom directives
watchers
scope inheritance
Enter fullscreen mode Exit fullscreen mode

The difficult part wasn't replacing syntax.

It was answering:

Where does this value come from?

Who changes it?

Which watcher reacts to it?

Does another parent scope affect it?
Enter fullscreen mode Exit fullscreen mode

A syntax conversion tool can help with repetitive work.

It cannot automatically explain years of hidden application behavior.


7. Custom Directives Became Mini Migration Projects

Simple directives moved easily.

Older directives often contained much more:

Isolated scopes
DOM manipulation
Watchers
Transclusion
jQuery plugins
Event listeners
Manual cleanup
Shared services
Enter fullscreen mode Exit fullscreen mode

Those couldn't be treated as:

directive → component
Enter fullscreen mode Exit fullscreen mode

without understanding what they were doing.

For each directive, we asked:

Is this still required?

Can the browser handle this natively now?

Should it become an Angular component?

Should part of the logic become a service?

Can we delete it entirely?
Enter fullscreen mode Exit fullscreen mode

One of the best migration outcomes was frequently:

We don't need this anymore.


8. Routes Became Useful Migration Boundaries

Routing gave us an easy way to visualize progress.

For example:

/dashboard            AngularJS
/customers            Angular
/customers/:id        Angular
/orders               AngularJS
/reports               AngularJS
/settings              Angular
Enter fullscreen mode Exit fullscreen mode

The implementation could temporarily be hybrid.

The user experience shouldn't feel hybrid.

Every migrated route reduced the amount of AngularJS that had to remain active.

That also gave the team a much clearer progress metric than counting rewritten files.


9. Tests Became the Migration Safety Net

Large migrations become dangerous when nobody knows which legacy behaviors are intentional.

Before replacing critical areas, we tried to capture their current behavior.

For example:

Given:
A user without approval permission

When:
They attempt to approve a high-value order

Then:
The operation must be blocked
Enter fullscreen mode Exit fullscreen mode

That business behavior matters.

Whether the new implementation uses the same function names does not.

Our migration workflow became:

Understand behavior
        ↓
Protect important behavior
        ↓
Migrate implementation
        ↓
Run tests
        ↓
Validate the workflow
Enter fullscreen mode Exit fullscreen mode

That was significantly safer than relying on manual browser checks after every change.


10. CSS Was a Migration of Its Own

We underestimated CSS.

A lot.

Legacy styles included selectors like:

.orders-page .panel .row .item span {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Those rules depended heavily on old DOM structures.

Once component markup changed, seemingly unrelated styles could break.

We discovered:

Global selectors
Specificity conflicts
Dead CSS
Duplicated rules
Old framework utilities
Styles tied to DOM hierarchy
Enter fullscreen mode Exit fullscreen mode

Instead of carrying every style forward, we treated styling as its own modernization task.

Modern components gained clearer ownership of their presentation.

That reduced accidental dependencies between screens.


11. Third-Party Dependencies Needed Their Own Decisions

A mature AngularJS system usually contains more than AngularJS.

For every dependency, we classified it as:

Keep
Upgrade
Replace
Remove
Investigate
Enter fullscreen mode Exit fullscreen mode

Old packages might be:

  • Unmaintained
  • AngularJS-specific
  • Dependent on jQuery
  • Replaced by browser APIs
  • Incompatible with modern tooling

The important rule became:

Don't migrate a dependency simply because the legacy system happens to use it.

Modernization sometimes means rewriting.

It also sometimes means deleting.


12. Build Tooling Was Part of the Migration

Application code was only one part of the system.

The old project also carried things like:

Gulp
Webpack customizations
Legacy npm scripts
Environment injection
Asset pipelines
Testing infrastructure
Deployment scripts
Enter fullscreen mode Exit fullscreen mode

A migration therefore had to account for:

Local development
CI/CD
Testing
Production builds
Environment configuration
Source maps
Monitoring
Deployment
Enter fullscreen mode Exit fullscreen mode

A feature isn't truly migrated if the team can't build, test, and deploy it reliably.


13. Hybrid Architecture Was Useful—but Temporary

Running two frameworks gave us a safer migration path.

It also had a cost.

For part of the migration, the application had to support:

AngularJS runtime
+
Angular runtime
+
Interoperability
Enter fullscreen mode Exit fullscreen mode

So we watched:

  • Startup time
  • Bundle size
  • Memory
  • Shared services
  • Route transitions
  • Change-detection behavior

The hybrid architecture was never supposed to become the destination.

Every Angular migration should reduce the reason AngularJS needs to remain.

Otherwise:

Temporary hybrid application
Enter fullscreen mode Exit fullscreen mode

can quietly become:

Permanent hybrid application
Enter fullscreen mode Exit fullscreen mode

14. Legacy Modernization Was Bigger Than a Framework Upgrade

This was probably the biggest shift in thinking.

We began the project believing:

AngularJS
   ↓
Angular
Enter fullscreen mode Exit fullscreen mode

was the migration.

It wasn't.

The actual transformation included:

Legacy framework
+
Old dependencies
+
Technical debt
+
Build assumptions
+
Shared business rules
+
Outdated architecture
        ↓
Modernized application
Enter fullscreen mode Exit fullscreen mode

That is why a large AngularJS migration often behaves more like an application-modernization program than a simple framework upgrade.

A phased approach is especially useful for large legacy systems because it lets teams modernize individual areas, validate behavior, and keep the existing application operational while the replacement grows. SDLC Corp describes the same broader phased approach on its Legacy Software Modernization Services page.

The important principle is:

Reduce legacy risk incrementally instead of moving all of it into one massive release.


15. We Deleted AngularJS Continuously

One of the most satisfying metrics wasn't Angular code added.

It was AngularJS code removed.

Once a migrated feature was stable, we removed:

Old controller
Old template
Legacy route
Unused service methods
Dead CSS
Old tests
Unused dependency
Enter fullscreen mode Exit fullscreen mode

Migration progress started looking like:

Early:
AngularJS ███████████████
Angular   ██
Enter fullscreen mode Exit fullscreen mode

then:

Later:
AngularJS ████
Angular   █████████████
Enter fullscreen mode Exit fullscreen mode

Keeping the old implementation "just in case" creates ambiguity about which version is authoritative.

Once the replacement is validated, delete confidently.

Git already remembers what used to exist.


16. We Changed How We Measured Progress

Lines migrated weren't enough.

Some weeks added large amounts of Angular code.

Other weeks mostly removed old AngularJS.

Others focused almost entirely on testing or infrastructure.

Better metrics included:

Routes fully migrated

Legacy modules removed

AngularJS directives removed

Critical workflows covered by tests

Old dependencies eliminated

Legacy bundle contribution reduced

Shared AngularJS services with no remaining consumers
Enter fullscreen mode Exit fullscreen mode

Those metrics answered the question we actually cared about:

Are we getting closer to being able to remove AngularJS?


What Actually Took the Time

Looking back, the hard part wasn't writing TypeScript.

The work looked more like:

Understand legacy behavior
        ↓
Map dependencies
        ↓
Create framework boundaries
        ↓
Protect behavior with tests
        ↓
Migrate complete features
        ↓
Modernize styles
        ↓
Replace dependencies
        ↓
Update tooling
        ↓
Validate production behavior
        ↓
Delete legacy code
Enter fullscreen mode Exit fullscreen mode

Code conversion was only one step.


The Migration Workflow That Worked Better

The process eventually became repeatable:

1. Pick a bounded feature

2. Map its dependencies

3. Document important behavior

4. Add missing regression coverage

5. Define the Angular/AngularJS boundary

6. Implement the Angular version

7. Bridge shared services where necessary

8. Run automated tests

9. Validate real workflows

10. Switch feature/route ownership

11. Monitor production

12. Delete the AngularJS implementation

13. Repeat
Enter fullscreen mode Exit fullscreen mode

Nothing about this workflow is dramatic.

That's one of its strengths.

Large migrations become safer when individual releases become boring and predictable.


What We'd Do From Day One

If we started another migration of this size, we'd establish five rules immediately.

Map dependencies before estimating

Lines of code measure size.

Dependencies reveal complexity.

Establish the hybrid boundary early

Don't let every team invent a different migration mechanism.

Migrate vertical slices

Move complete capabilities instead of unrelated files.

Protect behavior with tests

Modernization should change implementation without unintentionally changing business rules.

Delete AngularJS continuously

The migration is complete when AngularJS can be removed—not simply when enough Angular has been written.


A Practical AngularJS Migration Checklist

Before migrating a feature:

[ ] Do we know which route owns it?

[ ] Do we understand its dependencies?

[ ] Are important business rules documented?

[ ] Do we know which AngularJS services it consumes?

[ ] Are critical behaviors protected by tests?

[ ] Have third-party dependencies been reviewed?

[ ] Is the Angular/AngularJS boundary clear?

[ ] Is CSS ownership understood?

[ ] Can we build and deploy the migrated feature reliably?

[ ] Do we know which legacy code can be deleted afterward?
Enter fullscreen mode Exit fullscreen mode

After migration:

[ ] Automated tests pass

[ ] Real workflows have been validated

[ ] Production monitoring looks healthy

[ ] Old route ownership is removed

[ ] Legacy controller/component is deleted

[ ] Dead CSS is removed

[ ] Unused service methods are removed

[ ] Obsolete dependencies are removed
Enter fullscreen mode Exit fullscreen mode

The Biggest Lesson

Migrating 400,000 lines from AngularJS wasn't really about converting 400,000 lines.

It was about gradually removing 400,000 lines worth of accumulated assumptions.

Assumptions about:

Routing
State
Dependency injection
DOM structure
Shared services
Build tooling
Testing
CSS
Third-party libraries
Business behavior
Enter fullscreen mode Exit fullscreen mode

Once those assumptions became visible, they could be addressed one boundary at a time.

That was the positive part of the migration.

We didn't simply end up with a newer framework.

We ended up understanding the application better than we did before the migration started.

And that changed our definition of a successful AngularJS migration.

Success wasn't only:

AngularJS code = 0
Enter fullscreen mode Exit fullscreen mode

It was:

Legacy risk reduced
+
Architecture clearer
+
Tests stronger
+
Dependencies understood
+
Deployments predictable
+
AngularJS removable
Enter fullscreen mode Exit fullscreen mode

A 400k-line migration looks intimidating when viewed as one rewrite.

It becomes much more achievable when it is turned into a sequence of small, measurable removals.


Top comments (0)