You ship a small bug fix. Suddenly, two other features break. Every deployment feels like gambling. The business depends on this app — it brings in revenue, customers use it daily — but nobody feels confident working on it.
You open a file that should be a simple list of functions and find a 2,000‑line monolith of nested loops and if‑statements. Comments like // Temporary fix from years before. Presentation, database queries, and business logic all mixed together in what one developer described as a “glorious spaghetti mashup.”
You're not alone. And your codebase isn't hopeless.
The Industry’s Dirty Secret
- ~13% of Composer installs still run end‑of‑life PHP versions
- ~27% are on EOL or security‑only versions
- Over 50% of popular PHP packages support unsupported PHP versions
Legacy systems are the norm, not the exception.
The Trap: Rewrite or Suffer
- Rewrites have ~23% success rate
- Incremental modernization has ~53% success rate
- Small changes fail only ~4% of the time
Rewrites discard years of encoded business knowledge.
The Third Path: Incremental Transformation
Instead of rewriting everything:
- Identify a painful area
- Extract logic into a message handler
- Test it in isolation
- Make it async if needed
- Repeat
This is exactly what Ecotone enables.
Example: The 800‑Line Controller
class OrderController
{
public function placeOrder(Request $request)
{
// validation, persistence, payments, emails, analytics...
}
}
Step 1: Extract an Event
class OrderWasPlaced
{
public function __construct(
public string $orderId,
public string $customerId,
public float $total
) {}
}
class OrderNotificationHandler
{
#[EventHandler]
public function sendConfirmation(OrderWasPlaced $event): void {}
}
Step 2: Publish From Legacy Code
$this->eventBus->publish(new OrderWasPlaced(
$order->id,
$customer->id,
$order->total
));
Step 3: Test in Isolation
class OrderNotificationHandlerTest extends TestCase {}
Step 4: Make It Async
#[Asynchronous('notifications')]
#[EventHandler]
public function sendConfirmation(OrderWasPlaced $event): void {}
Step 5: Repeat
Analytics, loyalty points, reporting — each becomes isolated and testable.
Command Handlers
class PlaceOrder {}
class OrderHandler {}
$this->commandBus->send(new PlaceOrder(...));
Database Queries
interface OrderQueries
{
#[DbalQuery(...)]
public function getPendingOrders(string $customerId): array;
}
Resilience & Retries
Retries, DLQs, and error handling are declarative and consistent.
Idempotency
#[Deduplicated('orderId')]
#[CommandHandler]
public function placeOrder(PlaceOrder $command): string {}
Why This Works
- System keeps running
- Small PRs
- Fast tests
- Compounding improvements
- Attracts senior engineers
Getting Started
composer require ecotone/ecotone
Laravel and Symfony integrations available.
Conclusion
Legacy codebases are not failures — they are assets.
Incremental modernization gives them a future.
Top comments (0)