Skip navigation EPAM
Dark Mode
Light Mode

AI for modernization: How to extract business capabilities from legacy code with AI

When I was starting out as a developer in the late 2000s, one of the must-have books was Robert Martin's Clean Code, and we all applied it. Meaningful naming, single responsibility, low coupling, high cohesion, all of it aimed at making code readable to the next person who touched it. Systems were structured so that anyone joining the team could understand what the software was doing without needing a guided tour. Different era, before “vibe coding” became a thing.

That code is now 10-15 years old. Legacy by any standard. But the discipline we put into it is exactly what makes AI-assisted discovery possible today.

Packages are named orders, customers, payments because someone insisted on it. Endpoints describe operations and frontend routes map to user journeys. The signals are there because developers put them there deliberately.

The problem has never been absence of information. It has been the effort required to reconstruct it reliably.

This is where AI changes the equation. Instead of manually piecing together intent from scattered files, teams can now surface patterns, relationships, and capabilities directly from the codebase itself, using those same signals that were written years ago with discipline.

In this post, I’ll walk through how to build a complete business capability map from legacy code, step by step. What used to take days or even weeks of fragmented analysis can now be done in a focused session, with better consistency and coverage. 

The capability extraction pipeline: How AI reads legacy code in 7 steps 

I use a 200K+ line digital banking codebase as a running example throughout. The capability extraction pipeline has 7 steps:

capability-workflow.md


A1: Seed Candidates (6 steps)
A1.1 Package structure analysis
A1.2 Database schema analysis ← skipped if no DB access
A1.3 Backend entry point analysis
A1.4 Frontend/UI entry point analysis ← skipped if API-only
A1.5 Merge signals from all sources
A1.6 Format candidate list


A2: Analyze Candidates (per candidate)
A2.1 Deep candidate analysis ← cohesion, coupling, boundaries
A2.2 Action determination ← confirm/split/merge/de-scope
A2.3 Consolidate all actions


A3: Verify Coverage
A3.1 Coverage check ← target >90%
A3.2 Orphan resolution


A4: Lock L1 Capabilities
A4.1 Finalize L1 list


A5: L2 Sub-Capabilities (per L1)
A5.1 Scope L2 candidates
A5.2 Refine L2 analysis
A5.3 Lock L2 list


A6: Generate Domain Model
A7: Industry Blueprint Comparison


Each step reads one input, produces one output, and feeds the next. If context breaks – and on large codebases it will – you resume from the last completed step. Outputs are written to files, so nothing is lost.

There are two design choices here that make the pipeline resilient in practice: 

1. It is adaptive by default. The pipeline does not assume perfect visibility into the system. If database access is unavailable, schema analysis is skipped. If there is no frontend layer, UI entry point analysis is removed. The sequence adjusts without breaking the overall flow. Capability extraction continues, just with fewer signals contributing to confidence.

2. Each step accepts optional pre-generated inputs: feed it a package structure export from nDepend, a database schema from your DBA, entry points from JetBrains analyzers, or your own architecture notes. These inputs are not required, but they anchor the analysis in higher-quality signals. The model does less guesswork and more verification.

A1- Signal extraction from legacy code: The foundation of capability mapping

The first four steps (A1.1–A1.4) extract signals independently from three sources:

  • Package structure (A1.1) – what the code is organized around. payments, customers, lending are strong signals. processing or management are ambiguous.
  • Database schema (A1.2) – what business data is stored. Table clusters, foreign key relationships, stored procedure groupings. In legacy systems, the database often reveals business domains more clearly than the code.
  • Entry points (A1.3, A1.4) – what operations are exposed and what users actually do. Backend: REST controllers, scheduled jobs, message consumers. Frontend: pages, navigation structure, feature folders. The key rule: group by business operation, not by technical type. A PaymentController, a RecurringPaymentJob, and a MoneyTransferPage aren't three candidates, they're all evidence of the same "Payments" capability.

Each source is analyzed independently before merging (A1.5) – a candidate that appears across multiple steps carries a strong signal, one that shows up in only one gets a medium or low confidence rating.

The output: 15-25 raw candidates, each with a confidence rating and the evidence trail from all sources.

On the banking prototype, the pipeline produced 19 candidates with different signal strength:

capability-priority.md
HIGH (14):  Customer Onboarding, Account Management, Card Services,
Payments - Domestic, Payments - International,
Bill Payments, Transaction History,
Deposits - Personal, Deposits - Group, Deposits - Teen,
Lending - Instant Loans, Lending - NOU Loans,
Investment Management, Customer Management

MEDIUM (4): Current Accounts ← merge with Account Management?
Payments - Scheduling ← capability or feature of Payments?
Product Catalog ← business capability or infra?
Gamification ← no frontend routes, is it active?

LOW (1): Business Partnerships ← no frontend, no business logic

Each candidate is not just a label on a list. It comes with traceability built in, mapped code locations, linked entry points, and a confidence score grounded in how consistently it appears across signals.

More importantly, the pipeline does not pretend certainty where it does not exist. Candidates with weak or conflicting signals are explicitly flagged, along with the exact reason why they are ambiguous. Is this a standalone capability or a feature within another domain? Is it active in the system or just a leftover artifact? 

These are decisions for the architect, surfaced automatically.

A2: Converting candidates into business capabilities with AI

Each candidate is examined through cohesion, coupling, and boundary clarity, the same lens you would use when deciding whether something deserves to exist as an independent service. 

The goal is to align the system with how the business actually operates. Every candidate is forced into a decision: confirm it as a capability, split it into smaller ones, merge it with a broader domain, de-scope it as non-business, or flag it for deeper investigation. This is where structural signals meet business judgment.

Take Payments - Scheduling. On the surface, it looks complete. It has a dedicated microservice, its own REST controller, and even frontend routes. From the agent’s perspective, it qualifies as a capability. But that perspective is shaped by technical boundaries, not business meaning.

From a business lens, scheduling is not a separate capability. It is a variation of Payments, defined by an additional parameter, frequency. Treating it as independent would artificially fragment the domain. So it is demoted to Level 2 under Domestic Payments. The system may deploy it separately, but deployment boundaries do not define business capabilities.

In our case, 19 candidates consolidated into 16 Level 1 capabilities. A coverage check (A3) confirms every significant package is accounted for, and the L1 list is locked (A4) before going deeper.

A5: L2 extraction as the bridge between business capability and execution 

Level 1 defines the system at a functional level. It tells you what exists.

Level 2 defines the system at an operational level. It tells you what can actually be acted on, migrated, extended, or replaced.

This distinction matters because most modernization efforts fail at the handoff between abstraction and execution.

For example, “customer onboarding” as an L1 capability is useful for orientation, but it is too broad to assign, estimate, or deliver. L2 resolves that by breaking the capability into concrete business operations with clear scope and ownership. “Customer Registration & Account Provisioning” and “Identity Verification & KYC Compliance” are not labels, they are executable units of work.

Each L2 is grounded in the codebase. It is mapped to specific files, linked to the operations it performs, and tied to the entities it owns or manages. That mapping turns the capability model into something teams can use directly. 

BC-001-02
BC-001-02: Identity Verification & KYC Compliance

Description: Manages customer verification workflow through Fourthline integration — document upload, biometric validation, verification status tracking.

Key Operations:
- Create customer via Fourthline (POST /api/v1/origination/verification)
- Upload identity documents (front/back)
- Initiate verification jobs
- Poll verification status
- Handle verification callbacks

Code Location:
- fourthline-client/.../fourthline

Key Entities:
- PersonVerification (OWNSperson_verifications table)
- FourthlineCustomer (MANAGES — via Fourthline API)
- VerificationJob (TRACKS — via Fourthline API)

External: Fourthline (KYC provider)

A6: Building the domain model

The final output is a consolidated Domain Model, a single, traceable representation of what the system actually does. It captures both Level 1 capabilities and Level 2 operations, with each element backed by code-level evidence. In the document, every capability, operation, and boundary can be traced to specific files, entry points, and entities in the codebase. The model answers not just what exists, but where it lives and how it is implemented.

Here's what it looks like for the banking prototype:

Capability Hierarchy
Digital Banking Platform
├── BC-001: Customer Onboarding
│ ├── BC-001-01: Customer Registration & Account Provisioning
│ └── BC-001-02: Identity Verification & KYC Compliance
├── BC-002: Customer Profile Management
│ └── ...
├── BC-003: Account Management
│ └── ...
├── BC-004: Deposits - Personal
│ └── ...
├── BC-005: Deposits - Group
│ └── ...
└── ...

The level of detail in each L1 and L2, code locations, operations, and entity ownership, is what turns this into a working tool rather than a diagram. A capability map without traceability tells you “the system handles Payments.” Useful for slides.

With traceability, you know Payments spans 32 files across 2 packages, owns 4 entities, exposes 8 endpoints, and depends on Account Management through TransferService.cs.

That is a real boundary. That's what you hand to a team and say "this is your migration slice." [Check out the appendix for a full capability entry]. 

When you need to go deeper, you do not point the AI at the entire 200K-line codebase. You constrain it. Tools like Cursor IDE or GitHub Copilot work best when the scope is tight. Feed them the 32 files that define Payments, and the output quality improves immediately. From there, you can generate functional specs, extract business rules, and trace validation logic. The pattern stays the same. Scope first, then analyze. The Domain Model tells you where to look. The tools do the rest.

A7: Benchmarking the domain model against industry capability standards

Once the code-derived domain model is locked, the final step is comparison against an industry capability reference such as Banking Industry Architecture Network for banking, TM Forum for telecom, or APQC for cross-industry. This is not validation, the code only remains the source of truth. This step adds context.

The comparison highlights three things:

  • Which capabilities align with industry standards. 
  • Which are specific to this organization. 
  • And which expected industry capabilities are missing from the codebase.  That last category is the most useful. It drives targeted questions. Is this handled by an external system, or is it a genuine gap?

In the banking prototype, the BIAN comparison showed strong alignment on core capabilities, but no code-level presence for fraud detection or regulatory reporting. That changes the conversation. It moves from system understanding to capability positioning within the industry.

Why not just use an AI assistant with a good prompt (or what happens without the pipeline) 
 

I ran the same repository through a single structured prompt in GitHub Copilot, using a six-phase checklist covering reconnaissance, entry points, domain model extraction, capability mapping, dependency analysis, and report generation.

The execution was strong. It identified all 28 services, counted lines of code correctly, and catalogued controllers and entities with good coverage. But when I compared the result to the pipeline output, the gap was clear. Roughly half of the Level 1 capabilities aligned, and fewer than half of the Level 2 outputs mapped to real business operations. The rest were delivery channels, infrastructure layers, tooling, or cross-cutting concerns presented as business capabilities.

Misclassified capability output
├── Mobile Banking Channel ← delivery channel, not a capability (87k LOC — 43% of codebase)
├── Core Banking Integration ← infrastructure layer
├── Administration & Oversight ← operational tooling
├── Demo & Test Environment ← test harness
├── Product Catalog ← infrastructure layer
├── Banking Relationship Contracts ← infrastructure layer
└── Customer Communications ← cross-cutting concern

A half-right output is more dangerous than a completely wrong one. A wrong model gets challenged in the first review. One that nails half the capabilities and misclassifies the rest reads professionally, looks credible, and gets nodded through unless someone in the room knows enough to catch it.

The issue isn't the model or the tool, both are excellent in the case. It's that a single pass makes judgment calls like "is this a business capability or infrastructure?" implicitly. Sometimes it gets them right, sometimes not. A multi-step pipeline forces those decisions at explicit checkpoints with human review. But even that won't catch everything, which is why you should run more than one tool.

A single run of GitHub Copilot with a well-structured prompt costs very little. A deeper pass using Cursor IDE, a session in Claude Code, or a full pipeline execution adds more time, but also a different lens. Under each run, the value comes from comparing outputs, identifying where they agree, and isolating where they diverge. That divergence is where most of the important decisions sit.

This is no different from stakeholder discovery. You do not rely on a single interview to understand the business. You run several, knowing each person brings partial context and bias. The system is reconstructed through overlap, not individual accuracy.

Appendix: Domain model entry – Customer onboarding

BC-001 capability mapping
BC-001: Customer Onboarding                                      2 L2s
─────────────────────────────────────────────────────────────────────────
Orchestrates customer acquisition from registration through KYC
verification and account activation. Integrates with Fourthline
for identity verification.

L2 Operations:

BC-001-01: Customer Registration & Account Provisioning
Code: customers-origination-app/.../customersorigination
Entities: OWNS Customer, CREATES Person, PROVISIONS Account
Operations:
- Create customer account (POST /api/v1/origination/customers)
- Validate email/phone uniqueness
- Register customer in core banking
- Provision primary current account
- Create Wise user for international payments
- Trigger onboarding gamification quest

BC-001-02: Identity Verification & KYC Compliance
Code: fourthline-client/.../fourthline
Entities: OWNS PersonVerification, MANAGES FourthlineCustomer
Operations:
- Create customer via Fourthline
- Upload identity documents (front/back)
- Initiate verification jobs
- Poll verification status
- Handle verification callbacks
External: Fourthline (KYC provider)

Cross-Capability Dependencies:
BC-002 Customer Profile Management (creates Person entity)
BC-003 Account Management (provisions primary account)
Payments - International (creates Wise user)
Gamification (triggers onboarding quest)