Best practices for scalable architecture in startups

  • I'm repeatedly stumbling over the question: how much architecture is really useful at the beginning without getting yourself tangled up? 😅 Many say "keep it simple", but at the same time you don't want to have to rebuild everything later.

    My current thought: think modular to start with (clear boundaries between components), simple APIs, and e.g. start with a modular monolith instead of going straight to microservices. Then you can extract pieces later when it really hurts. How do you do it? Plan upfront, or intentionally go “quick & dirty” and refactor later?

    This post has been automatically translated.

  • I'll admit I'm a member of the "quick & dirty" camp ... with the "dirty" part carrying more weight :D

    Whether that's deliberate ... hard to say - I just "tinker" until it works. I'm more lacking the discipline to do it better.

    But I don't inflict my code on anyone for that reason either :P

    This post has been automatically translated.

    Fßnf von sechs Spielern haben bestätigt, dass Russisch Roulette total ungefährlich ist!

  • I wouldn’t demonize "quick & dirty" — a prototype can perfectly well look like the tangle of cables behind the TV. You should, however, have a rough idea beforehand which parts will likely move later: data model, auth, external APIs, etc. Clear module boundaries help more than a fancy microservice diagram.

    I once saw on a small project how we split services far too early. In the end most of the work was Docker, deployments and "why can’t Service A find Service B". A modular monolith felt much more relaxed afterwards. Refactor when it actually hurts — but leave time for it in the plan, otherwise Micha will stay permanently at "dirty" 😄

    This post has been automatically translated.

  • I’d add one more thing: don’t just plan module boundaries, but keep **measurability and decommissioning** cheap from the start. Structured logs, metrics, a few smoke/integration tests and clean DB migrations may seem boring at first, but they save a ton of time later when you actually need to scale.

    When you want to “extract later”, the database is often the real boss fight. If every module fiddles directly with the same tables, the monolith may be modular in code but not in its dependencies. So better assign ownership per area and go via internal APIs/events, even if everything still runs in the same deployment.

    This post has been automatically translated.

  • > “the database [is] often the real boss fight” – one hundred percent. Id4d take **reversibility** as an additional criterion for a startup: choose decisions you can change cheaply later. Feature flags, versioned API endpoints and as little infrastructure as possible that a single dev doesn't understand or can't operate.

    And don't park refactoring as “we'll do it sometime” — plan small fixed slots for it, e.g. after every major feature. Otherwise the next customer request always wins. Microservices for me are more like season two, not the pilot film 😄

    This post has been automatically translated.

  • “Microservices are more like season two” sums it up pretty well. I’d add one hard rule: Don’t split a service without a *concrete* reason, such as independent scaling, separate deployment cycles, or a team that actually works on it autonomously. “It might grow someday” isn’t enough; otherwise, you’re just buying distributed complexity in advance.

    With the modular monolith, I’d also enforce dependencies technically rather than merely drawing them in Confluence: separate packages/modules, no cross-access to other teams’ database tables, and clear ownership. If an area is meant to be extracted later, you can tell pretty quickly whether it’s already reasonably extractable—or was merely a pretty folder name.

    This post has been automatically translated.

  • I’m fully on board with “enforce technically.” Documented rules often become mere decoration during a stressful sprint 😄 Architecture tests can help a lot here, e.g. with ArchUnit for Java or corresponding dependency checks in other stacks. That way, an unauthorized import is flagged directly during the build rather than only when it’s extracted later.

    What has also helped me: ADRs for the few decisions that are genuinely important, but deliberately kept short – decision, context, consequence. Don’t document every detail, just the things that would otherwise lead to the same discussion again six months from now. Do you tend to use tools in the repo for that, or is a small collection of Markdown files enough for you?

    This post has been automatically translated.

  • When it comes to ADRs, I’ve become pretty pragmatic: Markdown in the repo is perfectly sufficient as long as the ADRs are discoverable and updated through PRs. A wiki outside the codebase becomes outdated much faster for us. I also think it’s important to have a small “architecture checklist” as part of the review: Who owns the data? Is there a prohibited cross-access? Is the decision reversible? That forces more clarity than a 40-page architecture document.

    Architecture tests aren’t a free pass either. They check dependencies, not whether the boundaries make sense from a domain perspective. That still requires real team and domain discussions—and occasionally the courage to rename or restructure a module. Besides ArchUnit/dependency checks, do you also use automated rules for database access, or is that mainly a matter of review discipline for you?

    This post has been automatically translated.

  • I’d also include **operational boundaries** in the architecture checklist: What happens with timeouts, duplicate events, partial deployments, or a broken migration? Especially with a modular monolith, you can otherwise be cleanly separated from a domain perspective, yet still create a huge cascading failure through synchronous calls. A few clear rules for timeouts, idempotency, and retries are worth establishing early—without putting Kafka in place right away 😉

    For ADRs, I also find it very useful to define an expiration date or a “review when X occurs” condition. So not just *why did we make this choice?*, but also *what signal would overturn the decision?* For example, at what level of load, team size, or deployment frequency should a module actually be extracted? Are you already using specific metrics for this, or is it more of a gut feeling?

    This post has been automatically translated.

  • Yes, operational boundaries are indeed completely missing from many architecture lists. I would run through them at least once for every module: What happens on timeout, retry, partial failure, and invalid data? To start with, explicit timeouts, idempotent commands, and a clean approach to migrations are often enough—you don’t need an event platform right away. Especially with retries, it’s important not to blindly execute something multiple times; otherwise, a minor error can quickly turn into a duplicate payment or something similar.

    With ADRs, I like the combination of “review when X occurs” and a few measurable signals: response times consistently exceeding the target, deployments regularly blocking one another, a module requiring its own scaling, or changes to it constantly causing side effects. That way, the architecture isn’t reworked based on gut feeling. I would even link these signals in the repo or monitoring so that the decision is visible later not only in the ADR, but also in day-to-day operations. Which operational scenarios would you actually test as mandatory at the start—only the critical business processes or every module?

    This post has been automatically translated.

  • For measurable signals, I would definitely use operational data, not just team sentiment: error rate, p95 latency, deployment duration, number of manual interventions, and perhaps the size of the affected changes. We used to “review” ADRs based on calendar dates, which was more of a ritual. A better approach was to trigger a review when, for example, a module regularly takes more than 30 seconds, more than one team needs to work on it, or deployments repeatedly have to be performed together because of coupling.

    What I also introduce early in a modular monolith is consistent logging with a correlation ID and simple metrics for each module. It costs little at the beginning but quickly shows where the actual boundaries lie. For database migrations, I use expand/contract whenever possible: first add the new structure in a compatible way, switch the code over, and remove the old column later. Has anyone had good experiences with automated architecture tests for database ownership rules like these? In my experience, that quickly becomes more difficult than ordinary package dependencies.

    This post has been automatically translated.

Participate now!

Don’t have an account yet? Register yourself now and be a part of our community!