AI Isn't the Problem. Your Architecture Is
AI coding assistants have created a velocity paradox. Developers ship code 55% faster, but that AI-generated code shows 41% higher churn rates (rotation of the code). When you're adding features at twice the speed, architectural mistakes compound at twice the rate too.
AI coding assistants have created a velocity paradox. Developers ship code 55% faster, but that AI-generated code shows 41% higher churn rates (rotation of the code). When you're adding features at twice the speed, architectural mistakes compound at twice the rate too.
This isn't an argument against AI tools—they're transformative. It's an argument for investing in architecture that can absorb rapid change without collapsing under its own weight.
Whatever practices you follow—AI multiplies them. Follow solid patterns, and you build resilient systems faster than ever. Follow ad-hoc approaches, and you accumulate technical debt at unprecedented speed.
The question becomes: how do you ensure AI multiplies the right patterns?
The acceleration is real—and it's changing everything
The Stack Overflow 2025 Developer Survey confirms this AI becomes new norm now: 84% of developers use or plan to use AI tools, with 51% using them daily.
But velocity without structure creates debt. GitClear's analysis of 211 million lines of changed code found AI-generated code has a 41% higher churn rate—code that gets reverted or substantially rewritten within two weeks. The DORA Report quantified this at organizational scale: 25% increase in AI adoption correlates with 7.2% decrease in delivery stability.
MIT professor Armando Solar-Lezama captured it precisely—AI is "a brand new credit card that will allow us to accumulate technical debt in ways we were never able to do before."
The pattern is clear: AI amplifies whatever architectural approach you've established. The solution isn't to slow down—it's to build on foundations where the default path leads to resilient code.
Why declarative abstractions matter for AI-assisted development
Here's what experienced developers are discovering: AI tools perform dramatically better when working with well-structured, pattern-driven code. One of the most effective way to get better performance day after day is to create context files: providing style guides, examples, and rules files.
Messaging frameworks provide structure at a deeper level than style guides. They encode architectural patterns into declarative conventions that AI tools recognize and reproduce correctly.
Consider what a declarative annotation communicates.
In Ecotone:
#[CommandHandler]
public function placeOrder(PlaceOrder $command): voidIn Axon:
@CommandHandler
public void handle(PlaceOrderCommand command)In NServiceBus:
public class PlaceOrderHandler : IHandleMessages<PlaceOrder>Each tells both humans and AI: this code handles commands, receives typed command objects, and the framework manages routing, serialization, and error handling.
When you ask AI to add "Order Canceling" it generates code that fits the established pattern—not because it was explicitly instructed, but because the pattern is unambiguous.
We will discuss three different Frameworks that provides abstractions on top of well known Messaging patterns: Ecotone for PHP, Axon Framework for Java/Kotlin, and NServiceBus for .NET.
- Ecotone embraces pure declarative configuration through PHP 8 attributes. Business code contains no framework base classes, no infrastructure interfaces—just attributes declaring intent. This creates the cleanest separation between what your code does and how the framework delivers it.
- Axon Framework follows a similar annotation-driven philosophy for its core patterns, with imperative APIs available when complex features require direct framework interaction. This hybrid approach balances simplicity for common cases with full control when needed.
- NServiceBus takes a more interface-driven approach typical of the .NET ecosystem, where handlers implement framework interfaces and configuration happens through fluent APIs. This style provides excellent IDE discoverability while maintaining the separation between business logic and messaging infrastructure.
The common thread: all three guide developers toward consistent, predictable patterns that AI tools can recognize and extend reliably.
Workflows: Where ad-hoc solutions create the most debt
Real business processes span multiple steps, services, and time periods. An order involves payment, inventory, shipping, and notifications—each potentially failing independently.
Without standardized workflow coordination, teams create ad-hoc solutions for each process, each with its own error handling, its own state management, its own failure modes.
This is exactly where AI-assisted velocity becomes dangerous. Generate code quickly without workflow patterns, and you get a proliferation of inconsistent approaches. Each new feature adds its own retry logic, its own compensation handling, its own way of tracking multi-step progress. The codebase becomes a patchwork of similar-but-different solutions.
One of the solution that Messaging frameworks provide to solve this are sagas—a standardized pattern for coordinating long-running processes. The framework handles state persistence, message correlation, timeout management, and failure recovery. Your code declares business logic; the infrastructure handles coordination.
In Axon, a saga declares its triggers through annotations:
@Saga
public class OrderFulfillmentSaga {
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderPlacedEvent event) {
// Saga starts, framework handles correlation and persistence
}
@SagaEventHandler(associationProperty = "orderId")
public void handle(PaymentReceivedEvent event) {
// Framework routes by orderId, manages state
}
@EndSaga
public void handle(OrderCompletedEvent event) {
// Saga ends, framework cleans up
}
}Ecotone uses the same declarative approach with PHP attributes:
#[Saga]
final class OrderFulfillmentSaga
{
#[Identifier]
private string $orderId;
#[EventHandler]
public static function startWhen(OrderWasPlaced $event): self { /* ... */ }
#[EventHandler]
public function whenPaymentReceived(PaymentWasReceived $event): void { /* ... */ }
}NServiceBus uses interface implementation with explicit correlation:
public class OrderFulfillmentSaga : Saga<OrderSagaData>,
IAmStartedByMessages<OrderPlaced>,
IHandleMessages<PaymentReceived>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
{
mapper.MapSaga(s => s.OrderId).ToMessage<OrderPlaced>(m => m.OrderId);
}
}The syntax differs, but the pattern is consistent: declare which events start, continue, and complete the workflow. Let the framework handle the rest. When AI generates saga code in these frameworks, it generates code that follows the established pattern—with proper correlation, lifecycle management, and state handling.
Event Sourcing: Audit trails as architectural default
Event sourcing stores every state change as an immutable event. Instead of updating "current state," you append events describing what happened. This creates complete audit trails, enables time-travel debugging, and makes system behavior explicitly traceable.
But event sourcing's real value in the AI era is subtler: it makes side effects explicit. Traditional state-stored systems hide what changed. Event-sourced systems make change visible by design. When AI generates code that emits events, it generates code that documents its own effects.
Ecotone provides native event sourcing where aggregates return events with pure functions from command handlers:
#[EventSourcingAggregate]
class Wallet
{
#[CommandHandler]
public function withdraw(WithdrawMoney $command): array
{
if ($command->amount > $this->balance) {
throw new InsufficientFundsException();
}
return [new MoneyWasWithdrawn($this->walletId, $command->amount)];
}
#[EventSourcingHandler]
public function applyWithdraw(MoneyWasWithdrawn $event): void
{
$this->balance -= $event->amount;
}
}Axon was purpose-built for event sourcing and follows the same separation:
@Aggregate
public class Wallet {
@CommandHandler
public void handle(WithdrawMoneyCommand cmd) {
if (cmd.getAmount() > this.balance) {
throw new InsufficientFundsException();
}
apply(new MoneyWithdrawnEvent(walletId, cmd.getAmount()));
}
@EventSourcingHandler
public void on(MoneyWithdrawnEvent event) {
this.balance -= event.getAmount();
}
}Command handlers emit events—they never modify state directly. The framework persists events to an append-only store and reconstructs aggregate state by replaying them. This separation forces explicit declaration of what changed.
Both frameworks also provide projections—transforming event streams into read-optimized views. In Ecotone:
#[Projection('wallet_balance', Wallet::class)]
class WalletBalanceProjection
{
#[EventHandler]
public function whenWithdrawn(MoneyWasWithdrawn $event, WalletBalanceRepository $repository): void
{
$repository->updateBalance($event->walletId, -$event->amount);
}
}In Axon:
@ProcessingGroup("wallet-balance")
public class WalletBalanceProjection {
@EventHandler
public void on(MoneyWithdrawnEvent event, @Autowired WalletBalanceRepository repository) {
repository.updateBalance(event.getWalletId(), -event.getAmount());
}
}Projections let you build specialized read models for different query needs—all derived from the same event stream, all automatically kept in sync.
NServiceBus takes a different approach—it's a messaging framework rather than an event sourcing framework. Teams using NServiceBus for event sourcing integrate with dedicated event stores like EventStore, Marten, or SqlStreamStore. NServiceBus then handles event distribution after the external store persists them:
public class WalletHandler : IHandleMessages<WithdrawMoney>
{
private readonly IEventStore _eventStore;
public async Task Handle(WithdrawMoney message, IMessageHandlerContext context)
{
var wallet = await _eventStore.LoadAggregate<Wallet>(message.WalletId);
var events = wallet.Withdraw(message.Amount);
await _eventStore.AppendEvents(message.WalletId, events);
foreach (var evt in events)
await context.Publish(evt);
}
}This separation of concerns—external store for event persistence, NServiceBus for reliable event distribution.
When AI generates code in event-sourced systems, it generates code that produces named, typed events describing what happened. The pattern guides toward explicitness even when the developer (or AI) doesn't consciously think about debugging and auditability.
Reliable communication: Infrastructure-level resilience
Distributed systems fail. Networks partition, services crash, databases timeout. Without framework support, each feature needs its own error handling—and AI-generated error handling tends toward optimistic happy paths.
This is where messaging frameworks provide perhaps their most important value: resilience that doesn't depend on individual implementation quality.
When you configure Ecotone's retry policy with delayed retries and dead letter handling:
#[ServiceContext]
public function errorConfiguration()
{
return ErrorHandlerConfiguration::createWithDeadLetterChannel(
'errorChannel',
RetryTemplateBuilder::exponentialBackoff(initialDelay: 1000, multiplier: 2)
->maxRetryAttempts(3),
DbalDeadLetterChannel::create('dead_letter')
);
}Or NServiceBus recoverability:
endpointConfiguration.Recoverability()
.Immediate(i => i.NumberOfRetries(3))
.Delayed(d => d.NumberOfRetries(2).TimeIncrease(TimeSpan.FromSeconds(30)));Or Axon's dead letter queue:
axon:
eventhandling:
processors:
order-processing:
dlq:
enabled: trueEvery handler in your application gains automatic resilience. You don't implement retries per feature. AI doesn't need to remember to add retry logic. The framework handles it at the infrastructure level.
The same applies to outbox patterns for atomic message publishing and deduplication for exactly-once processing. These are configured once and apply everywhere. They're not patterns that developers must remember to implement—they're infrastructure that protects every message automatically.
This matters enormously for AI-assisted development. When AI generates a message handler, it generates a handler protected by framework-level resilience. The AI doesn't need to understand distributed systems failure modes. The architecture handles it.
The multiplication effect
The throughput gains from AI coding assistants are real and substantial. Teams that harness them effectively will outpace those that don't. But "effectively" requires architecture that shapes what gets multiplied.
Without messaging frameworks, AI multiplies:
- Ad-hoc error handling that varies by feature
- Implicit coupling hidden in direct method calls
- State changes scattered across the codebase with no audit trail
- Custom workflow coordination for each multi-step process
With messaging frameworks, AI multiplies:
- Consistent handler patterns with standardized structure
- Explicit message contracts that document component boundaries
- Event-driven changes that create automatic audit trails
- Saga patterns that coordinate workflows predictably
The frameworks don't constrain what you can build—they guide how you build it. When you ask AI to add a feature, the surrounding patterns shape the generated code toward consistency, resilience, and maintainability.
Start with patterns, scale with confidence
The question isn't whether to use AI coding assistants—that ship has sailed. The question is what patterns AI will multiply in your codebase.
Messaging frameworks encode decades of enterprise patterns into declarative conventions. They separate business logic from infrastructure concerns. They provide resilience at the architecture level rather than the implementation level. They create codebases where the default path leads to robust, maintainable systems.
When you're shipping at AI-assisted velocity, these aren't nice-to-haves. They're the scaffolding that determines whether that velocity builds something lasting or accelerates toward collapse.
Whatever practices you follow, AI multiplies them. Choose patterns worth multiplying.