← Back

Software Architecture

The main enemy is accidental complexity. Every architecture decision should reduce cognitive load, enable parallel work, minimize change amplification, make decisions explicit, and be understandable by new team members.

Start with a Modular Monolith

Default to a modular monolith. It gives monolith speed with the structure to scale later.

  • Monorepo is the intermediate step — reuse without microservices complexity.
  • Microservices only when teams require complete isolation. Even then, it depends on the team and situation. Microservices add real complexity. Do not reach for them early.

Decision path: monolith → monorepo → microservices. Each transition is driven by constraints (team independence, deployment isolation), not taste.

Vertical Slices per Module

Each module follows: domain / application / infrastructure / shared (public API).

  • Each module owns its domain and exposes a clear public API through a Facade.
  • No cross-module direct imports. Use the Facade.
  • Domain never depends on infrastructure.
  • Application orchestrates.
  • Infrastructure implements.
  • No circular dependencies.

Screaming Architecture

The codebase should scream what the system does, not what framework it uses. Preferred patterns: Clean Architecture, DDD, Screaming Architecture.

  • Folder structure reveals the business domain, not the framework.
  • Framework concepts (Express, Next.js) stay in infrastructure — never in business logic.
  • Names reflect business language. Domain terminology is sacred.

Programming Style

Context-dependent. Clean Architecture + dependency injection → classes are the natural fit. Utilities or simple projects → functional programming is simpler.

  • Even with functions, group them into service objects: const SlackService = { findByMessageId, findByThreadId, ... }.
  • Strong typing, explicit data transformations, predictable behavior, side effects controlled — regardless of paradigm.
  • Explicit > implicit, boring > clever. If code is hard to explain simply, the design is wrong.

DDD Enforcement

  • Entities are behavioral, not data bags. They have methods and computed properties.
  • Entities are immutable. Always return new objects to avoid side effects.
  • Use object parameters when more than 2 arguments.
  • Domain DTOs expose computed methods. Avoid assignment-order dependency.
  • Facades are the only place where use cases can be combined.

Abstractions Grow Through Understanding

Good abstractions are not designed upfront. Build, learn the real boundaries, refine. You cannot skip this discovery with a prompt or upfront design.

  • PoCs: anything goes — speed wins.
  • Production: clear layers and boundaries are essential. Well-defined abstraction rules are how you trust AI-generated code.

Architecture Is a Team Exercise

Module ownership, clear boundaries, and reduced change amplification — these are architecture decisions that are also team decisions. The goal is systems that last 10+ years, enable independent teams, and reduce fear of change.

Architecture should change because constraints changed, not because engineers want something shinier.

Decision Criteria

When choosing an architecture approach, evaluate in this order:

  1. Team size and independence needs — can teams deploy and develop independently?
  2. Cognitive load — can a new engineer understand the module in a reasonable time?
  3. Change amplification — does changing one thing force changes in many places?
  4. Current phase — PoC demands speed, production demands structure.

Choose microservices when team isolation is the constraint. Choose monorepo when code reuse is the constraint. Default to modular monolith when uncertain.

Anti-patterns

  • Microservices for small teams — adds network complexity, deployment overhead, and distributed debugging without the team independence benefit.
  • Framework-driven folder structurecontrollers/, models/, services/ tells you nothing about the domain. Folders should be booking/, inventory/, payment/.
  • Shared modules between public and private APIs — prevents independent evolution and can leak sensitive data.
  • Circular dependencies between modules — signals wrong boundary placement. Refactor the boundary.
  • Premature abstraction — creating helpers and utilities for one-time operations. Three similar lines of code is better than a premature abstraction.

Experience Notes

Previously: multi-repo with separate services. Now: monorepo as the intermediate step. Moved because multi-repo created code duplication and deployment coordination overhead that was not justified by the team size.

The transition from monolith to modular monolith to monorepo to microservices is a real path — each step driven by specific constraints, not best-practice checklists. Skipping steps creates complexity without the problems that justify it.