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
we moved toward:
Understand
↓
Create boundaries
↓
Migrate incrementally
↓
Validate behavior
↓
Remove legacy code
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
and:
File B
500 lines
Authentication
Permissions
Shared state
Pricing rules
Five integrations
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
Then we added dependency relationships.
For example:
Order Entry
↓
Customer Service
↓
Pricing Service
↓
Permissions
↓
Shared API Client
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
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
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
Once that slice moved, ownership was clear:
Before:
AngularJS owns Customer Search
After:
Angular owns Customer Search
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
});
But it could be used by:
Customer screens
Orders
Billing
Reporting
Search
Admin
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
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
};
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
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">
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
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?
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
Those couldn't be treated as:
directive → component
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?
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
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
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
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 {
...
}
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
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
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
A migration therefore had to account for:
Local development
CI/CD
Testing
Production builds
Environment configuration
Source maps
Monitoring
Deployment
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
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
can quietly become:
Permanent hybrid application
14. Legacy Modernization Was Bigger Than a Framework Upgrade
This was probably the biggest shift in thinking.
We began the project believing:
AngularJS
↓
Angular
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
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
Migration progress started looking like:
Early:
AngularJS ███████████████
Angular ██
then:
Later:
AngularJS ████
Angular █████████████
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
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
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
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?
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
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
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
It was:
Legacy risk reduced
+
Architecture clearer
+
Tests stronger
+
Dependencies understood
+
Deployments predictable
+
AngularJS removable
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)