Every system has an architecture, even if nobody designed it. The difference is whether you chose it or whether it is the accidental result of hundreds of small decisions. And those decisions get paid for late: when the team grows, when a big client arrives, or when a “simple” feature takes three sprints because it touches half the system.
These are the ideas we apply when designing platforms that need to grow.
Architecture is a problem of change
Good architecture isn’t the one with the most diagrams or the trendiest technology. It is the one that lets you change the system with little risk and little cost. That’s why the first question isn’t technical: which parts of the business will change the most in the next two years?
Pricing rules, payment methods and third-party integrations tend to change a lot. User management or core invoicing, much less. Architecture should isolate what is volatile so that its evolution doesn’t drag the rest along.
Start with a modular monolith
For most new products we recommend a modular monolith: a single deployable application, internally split into modules with strict boundaries.
src/
billing/ → domain, use cases, adapters
inventory/
customers/
shared/ → utilities only, no business logic
Each module exposes a public interface and hides its implementation. No module reads another module’s tables directly. With that discipline you get almost all the benefits of microservices (independent teams, understandable code) without paying their operational cost: failing networks, eventual consistency, distributed traces and coordinated deployments.
Microservices solve an organizational problem, not a performance one. If you don’t have several teams blocking each other, you probably don’t need them yet.
When to split out a service
Extracting a module into an independent service makes sense when at least one of these conditions holds:
- Different scale: one part receives ten or a hundred times more load than the rest (for example, event ingestion or search).
- Different pace of change: a team needs to deploy several times a day without waiting for everyone else.
- Different technical requirements: a machine learning process in Python next to a Node.js backend, or a component with special security requirements.
If the boundaries of the modular monolith are well defined, that extraction takes weeks, not a rewrite.
Layers: the domain at the center
Inside each module we separate three responsibilities:
- Domain: the business rules, with no dependencies on frameworks, databases or external APIs. It is the most valuable code and the easiest to test.
- Application: the use cases that orchestrate the domain (“issue invoice”, “reserve inventory”).
- Infrastructure: database, queues, external providers. It connects to the domain through interfaces.
The golden rule: dependencies point toward the domain. Switching payment providers or databases should touch an adapter, not your business rules.
Data: the coupling nobody sees
The most dangerous coupling isn’t in the code but in the database. When five modules read and write the same table, any schema change becomes a negotiation.
Some practices that work for us:
- Each module owns its tables; the others query through its interface.
- To communicate changes between modules we use domain events (“InvoiceIssued”), with the outbox pattern when we need to guarantee the event is published.
- Migrations are backward compatible: first add, then migrate, and finally remove.
Documented decisions
Every important decision is recorded in an ADR (Architecture Decision Record): one page with the context, the options considered, the decision and its consequences. Six months later, when someone asks “why did we do it this way?”, the answer exists and doesn’t depend on anyone’s memory.
Observability from the first deployment
You can’t scale what you can’t see. From the first version we include:
- Structured logs with a request ID that runs through the whole system.
- Business and technical metrics: orders per minute, p95 latency, error rate.
- Alerts based on symptoms the user feels, not on every CPU spike.
Checklist
Before calling an architecture good, we check:
- Can we explain each module’s boundaries in one sentence?
- Can a new developer make a change in one module without understanding the whole system?
- Can we replace an external provider by touching a single adapter?
- Do we know, with data, where the current bottleneck is?
Architecture isn’t designed once: it is looked after every sprint. If your platform is becoming slow to change, let’s talk and review together where to start.