Architecture is not a destination.

Real systems do not evolve in neat diagrams. They grow with the product, reflect team size, delivery pressure, business constraints, and the assumptions people had at the time. Some choices age well. Some choices solve one problem and create another.

That does not mean the earlier decision was bad. It means the context changed.

Good architecture is not about defending a pattern. It is about continuously asking whether the current shape of the system still helps the team deliver value safely and clearly.

Context

The platform started as a Laravel monolith — and that was the right call. It kept the system easy to reason about. Business rules lived close together. Local development was simple. The team could move quickly while the product was still changing and domain boundaries were not yet clear.

The problem was not that a monolith existed. The problem started when it grew beyond the team's ability to change it confidently.

Different parts of the system began moving at different speeds. Some areas needed different scaling characteristics. Some workflows became riskier to deploy together. Teams started stepping on each other's work. Testing became heavier. A small change in one area created anxiety in another.

The architecture was telling us something — not necessarily “split everything into services,” but clearly “the current boundaries are no longer clear enough.”

Problem

The main issue was coupling — not just technical coupling, but organisational coupling. Multiple product teams shared one deployment unit. A backend change for one workflow required coordination across teams, full regression testing, and a shared release window.

Deploy time was around 14 minutes. This might seem acceptable in isolation, but in practice it meant engineers batched changes, avoided small fixes, and delayed feedback loops. Confidence in deployments was low.

There was also a knowledge problem. As the monolith grew, fewer people understood the full system. Onboarding new engineers took longer. Making a change in an unfamiliar area felt risky because the boundaries between domains were implicit, not enforced.

Constraints

This was a live trading platform. We could not stop the product, run a full rewrite, and switch everything over at once.

  • All migrations had to happen alongside normal product delivery.
  • Active trading workflows could not be interrupted.
  • Data consistency and audit requirements had to be preserved throughout.
  • The team needed to keep shipping features while the platform changed beneath them.
  • We had to avoid creating a “big bang” migration that would stall delivery for months.

What I did

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

The act of extracting services forced the right conversations. What is the actual business capability here? Who owns this workflow? Where does this data belong? What needs to scale separately? Which changes should be deployable independently?

We ended up with twelve services — not because twelve was a target, but because the domain had twelve natural boundaries once we started looking carefully.

  • Extracted services incrementally, starting with the highest-churn domains.
  • Introduced explicit API contracts between services where implicit coupling had been hiding.
  • Moved to independent deployment pipelines per service with Kubernetes.
  • Replaced shared database access patterns with owned data stores and event-driven synchronisation where appropriate.
  • Built internal tooling to make local development with multiple services 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. We were deliberate about where that cost was worth paying.

A service boundary is only worth it when the independence it creates is more valuable than the complexity it introduces. In some parts of the system that was clearly true. In others, we resisted the pressure to split things just because the diagram looked cleaner that way.

  • Used Kafka for event-driven flows where async processing genuinely improved the system.
  • Kept synchronous boundaries where latency and consistency mattered more than independence.
  • Standardised observability across services early — tracing, structured logging, alerting — so distributed debugging stayed manageable.
  • Introduced AWS CDK for infrastructure as code so every environment was versioned and reproducible.
  • Built shared libraries for cross-cutting concerns so teams were not solving the same problems twelve times.

Outcome

Deploy time fell from 14 minutes to around 90 seconds per service. Teams could ship independently without coordinating release windows. Ownership became clearer. Onboarding improved because the boundaries were explicit and each service was smaller and easier to understand in isolation.

The platform also became easier to evolve further. When requirements changed — and they always do — the cost of changing a single service was much lower than the cost of changing the whole monolith. That is what good architecture should do: reduce the cost of the next change.

What I learned

The biggest lesson was that no pattern deserves loyalty forever. The monolith was the right starting point. The services were the right learning step. Moving toward a more pragmatic serverless model with AWS CDK later was the right next direction — not because serverless is always better, but because the operational overhead of managing twelve services had grown beyond the value it was providing at that product stage.

I also learned that reversing direction is not failure. Sometimes a team moves from a monolith to services and later realises that some of those services should be brought closer together again. That is learning, not retreat.

Architecture affects how people work — how quickly new engineers can contribute, how confident people feel during deployment, how much context someone needs before making a change. A clean diagram is not enough. The architecture has to work for the people maintaining it.

Reflection

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

The best architecture is not the one that looks most modern. It is the one that helps the team understand the product, ship safely, operate confidently, and change direction when reality changes.

Not architecture as ideology. Architecture as a flow.