Architecture is not a destination.

Real systems don't evolve in neat diagrams. They grow with the product, and they carry the marks of team size, delivery pressure, and whatever everyone assumed was true at the time. Some of those choices age well. Some solve one problem and quietly create another.

That doesn't make the earlier decision wrong. It means the context moved.

Good architecture isn't about defending a pattern. It's about asking whether the current shape of the system still helps the team ship safely.

Context

The platform started as a Laravel monolith, and that was the right call. Business rules lived close together. Local development was simple. The team could move fast while the product was still changing and the domain boundaries weren't obvious yet.

The problem wasn't that a monolith existed. The problem started when it outgrew the team's ability to change it confidently.

Different parts of the system began moving at different speeds. Some needed to scale differently. Some workflows got riskier to deploy alongside others. Teams started stepping on each other. Testing got heavier, and a small change in one area created anxiety in another.

The architecture was telling us something. Not “split everything into services” — just that the boundaries were no longer clear enough.

Problem

The core issue was coupling, and not only the technical kind. Multiple product teams shared one deployment unit, so a backend change for one workflow meant coordination across teams, full regression testing, and a shared release window.

Deploys took around fifteen minutes. That sounds tolerable in isolation, but it changed behaviour: engineers batched changes, skipped small fixes, and let feedback loops stretch. Nobody deployed on a Friday.

There was a knowledge problem too. As the monolith grew, fewer people understood the whole thing. Onboarding took longer. Working in an unfamiliar area felt risky, because the boundaries between domains were implicit rather than enforced.

Constraints

This was a live trading platform. No stopping the product for a rewrite.

  • Every migration step had to happen alongside normal feature delivery.
  • Active trading workflows couldn't be interrupted.
  • Data consistency and audit requirements had to hold throughout.
  • No big-bang cutover that would stall delivery for months.

What I did

I led the incremental migration from the monolith toward a fleet of Node.js services, working with the team to find real domain boundaries rather than drawing lines on a whiteboard.

Extracting services forced the right conversations. What's the actual business capability here? Who owns this workflow? Where does this data belong? What needs to scale on its own?

We ended up with twelve services — not because twelve was a target, but because that's how many natural boundaries the domain had once we looked properly.

  • Extracted incrementally, starting with the highest-churn domains.
  • Introduced explicit API contracts where implicit coupling had been hiding.
  • Moved to independent deployment pipelines per service on Kubernetes.
  • Replaced shared database access with owned data stores, and event-driven sync where it made sense.
  • Built internal tooling so local development across multiple services stayed manageable.

Engineering decisions

The cost of distribution is real. A function call becomes a network call. A simple transaction becomes a distributed workflow. A local bug becomes a tracing problem. I tried to be deliberate about where that cost was worth paying.

A service boundary earns its keep when the independence it creates is worth more than the complexity it adds. In some parts of the system that was obviously true. In others I pushed back on splitting things just because the diagram looked tidier that way.

  • Used Kafka for event-driven flows where async genuinely helped.
  • Kept synchronous boundaries where latency and consistency mattered more than independence.
  • Standardised observability early — tracing, structured logging, alerting — so distributed debugging didn't become guesswork.
  • Introduced AWS CDK for infrastructure as code, so every environment was versioned and reproducible.
  • Built shared libraries for cross-cutting concerns, so teams weren't solving the same problem twelve times.

Outcome

Deploys went from around fifteen minutes to under two, per service. Teams could ship without negotiating a release window. Ownership got clearer. Onboarding improved, because the boundaries were explicit and each service was small enough to hold in your head.

The platform also got easier to change. When requirements moved — and they always do — the cost of changing one service was far lower than the cost of changing the monolith. That's the real test of an architecture: what does the next change cost?

What I learned

No pattern deserves loyalty forever. The monolith was the right starting point. The services were the right next step. Moving toward a more pragmatic serverless model with AWS CDK after that was the right direction again — not because serverless is better, but because operating twelve services had started costing more than the independence was worth at that stage of the product.

Reversing direction isn't failure. Sometimes a team splits into services and later finds that some of them belong closer together. That's learning, not retreat.

And architecture shapes how people work — how fast a new engineer can contribute, how confident anyone feels hitting deploy, how much context you need before touching something. A clean diagram doesn't capture any of that.

Reflection

This project gave me a lot of respect for the human side of architecture. The technical decisions mattered, but so did the arguments about ownership, naming, team autonomy, and what we actually needed versus what looked good on a slide.

The best architecture isn't the most modern one. It's the one that helps the team understand the product, ship safely, and change direction when reality does.

Not architecture as ideology. Architecture as flow.