You know the one. That service class that started out small and reasonable, and then kept growing. Sprint after sprint, feature after feature. Until one day you open it and realize: this thing is 800 lines and does everything.
In my case it was OrderProcessingService.cs. It validated orders, calculated prices, managed statuses, sent notifications, and probably made coffee too. It had been on the refactoring backlog for weeks. Nobody wanted to touch it.
Why manual refactoring is painful
Splitting a large service isn’t technically hard. You know what needs to happen: identify the responsibilities, create new classes, extract the logic, wire up dependency injection, and update the tests.
But the execution is where it gets tedious. You need to carefully trace which methods depend on which private fields. You need to figure out which groups of methods form a cohesive responsibility. You need to make sure you don’t break anything in the process — and in an 800-line file, there are plenty of places where things can quietly go wrong.
It’s the kind of task where you think “this will take an hour” and then it’s 4 PM and you’re still fixing compilation errors.
Step 1: Ask for analysis
I opened my terminal in the project and started Claude Code. First question:
“Analyze OrderProcessingService.cs. What distinct responsibilities does this class have?”
Claude Code opened the file, read all 800 lines, and came back with a breakdown:
- Order validation — checking stock, validating addresses, verifying customer credit limits
- Price calculation — base prices, discounts, tax calculation, shipping costs
- Status management — state transitions, status history, notification triggers
- Notification dispatch — emails, webhook calls to external systems
Four clear responsibilities in one class. I already had a hunch, but seeing it laid out with specific method names attached to each group made it concrete.
Step 2: Plan before you cut
I didn’t want Claude Code to just start hacking away. So I switched to Plan Mode (Shift+Tab) and asked:
“Create a plan to split this service into separate classes following the Single Responsibility Principle. Keep the original service as an orchestrator.”
Claude Code proposed:
IOrderValidator+OrderValidator— all validation logicIPriceCalculator+PriceCalculator— pricing and discount logicIOrderStatusManager+OrderStatusManager— state transitions and history- Keep
OrderProcessingServiceas a thin orchestrator that coordinates the three
Good plan. I approved it.
Step 3: Let Claude Code execute
Here’s where it gets interesting. Claude Code didn’t just move methods around. It:
- Created the interfaces with the right method signatures
- Extracted the implementations, including the private helper methods each group needed
- Updated
OrderProcessingServiceto inject the new dependencies - Registered everything in
Program.cs
The original service looked something like this (simplified):
public class OrderProcessingService
{
private readonly AppDbContext _db;
private readonly ILogger<OrderProcessingService> _logger;
public OrderProcessingService(AppDbContext db, ILogger<OrderProcessingService> logger)
{
_db = db;
_logger = logger;
}
public async Task<OrderResult> ProcessOrderAsync(Order order)
{
// Validation (150+ lines of logic)
if (!await HasSufficientStock(order))
return OrderResult.Failed("Insufficient stock");
if (!IsValidShippingAddress(order.ShippingAddress))
return OrderResult.Failed("Invalid address");
if (!await CheckCreditLimit(order.Customer, order.Total))
return OrderResult.Failed("Credit limit exceeded");
// Price calculation (200+ lines of logic)
var basePrice = CalculateBasePrice(order.Items);
var discount = ApplyDiscounts(order.Customer, basePrice);
var tax = CalculateTax(order.ShippingAddress, discount);
var shipping = CalculateShipping(order.Items, order.ShippingAddress);
order.Total = discount + tax + shipping;
// Status management (100+ lines of logic)
order.Status = OrderStatus.Confirmed;
await RecordStatusChange(order, OrderStatus.Confirmed);
await NotifyCustomer(order);
await _db.SaveChangesAsync();
return OrderResult.Success(order);
}
// ... 20+ private methods for each responsibility
}
After the refactor, the interfaces were clean and focused:
public interface IOrderValidator
{
Task<ValidationResult> ValidateOrderAsync(Order order);
}
public interface IPriceCalculator
{
PriceBreakdown CalculatePrice(IReadOnlyList<OrderItem> items,
Customer customer, Address shippingAddress);
}
public interface IOrderStatusManager
{
Task TransitionStatusAsync(Order order, OrderStatus newStatus);
}
And the orchestrating service became straightforward:
public class OrderProcessingService
{
private readonly IOrderValidator _validator;
private readonly IPriceCalculator _calculator;
private readonly IOrderStatusManager _statusManager;
private readonly AppDbContext _db;
public OrderProcessingService(
IOrderValidator validator,
IPriceCalculator calculator,
IOrderStatusManager statusManager,
AppDbContext db)
{
_validator = validator;
_calculator = calculator;
_statusManager = statusManager;
_db = db;
}
public async Task<OrderResult> ProcessOrderAsync(Order order)
{
var validation = await _validator.ValidateOrderAsync(order);
if (!validation.IsValid)
return OrderResult.Failed(validation.ErrorMessage);
var pricing = _calculator.CalculatePrice(
order.Items, order.Customer, order.ShippingAddress);
order.Total = pricing.Total;
await _statusManager.TransitionStatusAsync(order, OrderStatus.Confirmed);
await _db.SaveChangesAsync();
return OrderResult.Success(order);
}
}
From 800 lines to about 30. The logic didn’t disappear — it moved to where it belongs.
Step 4: DI registration and verification
Claude Code also updated Program.cs:
builder.Services.AddScoped<IOrderValidator, OrderValidator>();
builder.Services.AddScoped<IPriceCalculator, PriceCalculator>();
builder.Services.AddScoped<IOrderStatusManager, OrderStatusManager>();
builder.Services.AddScoped<OrderProcessingService>();
Then it ran dotnet build. Clean compile. Then dotnet test. All 47 existing tests passed. No regressions.
The whole thing took about 40 minutes, including review time. That backlog item that nobody wanted to pick up? Done before lunch.
When this works well — and when it doesn’t
This kind of refactoring is Claude Code’s sweet spot. The responsibilities were clearly separable. Validation logic doesn’t need to know about price calculations. Status management doesn’t care about shipping costs. Clean boundaries, clean splits.
It gets harder when the responsibilities are intertwined. If your validation rules depend on the calculated price, which depends on the customer’s status, which gets updated during validation — that’s a different conversation. Claude Code will still help you think through it, but the split won’t be as clean, and you’ll need to make more judgment calls about where to draw the lines.
My advice: start with the easy wins. The service with four obvious responsibilities that everyone agrees should be separate. Build confidence with those before tackling the gnarly ones.
Tips for your own refactoring
Always start with analysis. Don’t jump straight to “refactor this.” Ask Claude Code to identify the responsibilities first. It often spots groupings you hadn’t considered.
Use Plan Mode. A refactoring plan you can review before execution saves you from undoing bad splits. Shift+Tab is your friend.
Keep the orchestrator. Don’t delete the original class. Transform it into a thin coordinator. This keeps existing callers happy and minimizes the blast radius of the change.
Run the tests after every step. Claude Code can do this for you. A
dotnet testafter each extracted class catches problems early, when they’re easy to fix.Review the interfaces. The method signatures Claude Code creates are usually sensible, but you know your domain better. If an interface feels wrong, say so — Claude Code will adjust.
Try it yourself
Got a service that’s been growing unchecked? Open your terminal:
claude
“Analyze [YourService].cs. What responsibilities does this class have? Propose a plan to split it.”
You might be surprised how quickly that backlog item goes from “nobody wants to touch it” to “done.”