Monolith vs Microservices — A Pragmatic Guide for Startups

by Arif Ikhsanudin, Backend Developer

The distributed systems tax you pay upfront

Your team has five engineers and a product that has been live for four months. You are debating whether to split the billing service into its own microservice. The argument for: independent deployability, clear ownership, the ability to scale it separately. The argument against: you do not know yet what billing actually needs to do. This is the most common and most costly mistake I see startups make.

What a monolith actually costs you

Nothing — until you have a genuine reason to pay it. A well-structured monolith with clear module boundaries is not a technical debt. It is a pragmatic choice that defers coordination costs until you understand your domain well enough to draw the right service boundaries.

The Rails engine pattern, for example, lets you build near-microservice isolation within a monolith. Each domain — billing, notifications, user management — lives in its own engine with its own models, controllers, and migrations. The communication happens via Ruby method calls instead of HTTP, which means no serialization overhead, no network latency, no distributed transaction complexity.

# Modular monolith — Billing engine
# Located at engines/billing/

module Billing
  class Engine < Rails::Engine
    isolate_namespace Billing
  end
end

# Clean boundary — other engines call Billing::SubscriptionService
# not BillingController or billing_subscriptions table directly
module Billing
  class SubscriptionService
    def activate(user_id:, plan_id:)
      subscription = Billing::Subscription.create!(
        user_id: user_id,
        plan_id: plan_id,
        status: :active,
        started_at: Time.current
      )
      Billing::InvoiceJob.perform_later(subscription.id)
      subscription
    end
  end
end

This is extractable into a microservice later. The interface is already defined. The data boundary is already clean. You pay none of the distributed systems cost during the period when you are still learning what the domain looks like.

What microservices actually cost you

Distributed systems introduce a class of failure modes that do not exist in monoliths: partial failures, network partitions, message ordering, idempotency requirements, distributed tracing overhead. These are solvable problems, but each one requires tooling, operational expertise, and developer time to solve.

A call stack in a monolith that takes 5ms becomes a chain of HTTP requests with 20-50ms overhead per hop in a microservices architecture. You fix this with gRPC (Protocol Buffers, multiplexed connections, ~2-5ms overhead) or by questioning whether the service boundary is right. Either way, it is a decision you now have to make.

Deployment complexity multiplies. A monolith has one deployment pipeline. Fifteen microservices have fifteen. Each needs its own CI configuration, Docker image, Kubernetes deployment manifest, health checks, secrets management, and rollback procedure. On a small team, this overhead crowds out product work.

# What one microservice adds to your infrastructure
# (multiply by number of services)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: billing-service
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: billing
        image: billing-service:{{ .Values.tag }}
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: billing-secrets
              key: database-url
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10

Multiply that manifest — plus the service definition, the ingress rules, the HPA configuration, the PodDisruptionBudget — by fifteen services and you have a full-time infrastructure engineering job that is not building product.

The actual decision criteria

Start with a monolith when:

  • You have fewer than ten engineers
  • The product is less than two years old and the domain model is still being discovered
  • You do not have dedicated DevOps/platform engineering capacity
  • Different parts of the system do not have wildly different scaling requirements

Consider extracting a service when:

  • A bounded context has a genuinely different scaling profile (e.g., a video transcoding pipeline next to a CRUD API)
  • Team ownership is creating merge conflicts and deployment coupling — your billing team cannot ship without coordinating with the auth team
  • A specific component has security or compliance requirements (PCI DSS, SOC 2) that benefit from a hard perimeter
  • You have proven the domain model is stable enough that the service interface will not need to change monthly

The rule that has served me well: Do not split a service until you have drawn the domain boundary in code first. If you cannot define the interface between two services as a clean set of method signatures — inputs, outputs, error cases — you do not understand the boundary well enough to pay the distributed systems cost.

The path from monolith to services

Extract the data layer first. Before splitting into services, ensure each domain manages its own database tables. Cross-domain joins in SQL are a sign the boundary is wrong or not yet clean. Once the data is isolated by domain, the service extraction is mechanical — the interface is already there, you are just adding an HTTP or gRPC transport in front of it.

The startups that do this well spend 12-18 months building a well-modularized monolith, then extract services one at a time based on actual operational pain, not anticipated scaling needs. The ones that start with microservices often spend 12-18 months debugging distributed failures in a system they do not yet understand.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Your Dockerfile Works But Your Image Is Bigger Than It Needs to Be

A working Dockerfile is not the same as a good one. Most images carry megabytes of unnecessary weight that slows builds, bloats registries, and widens the attack surface — and the fixes are straightforward once you know where to look.

Read more

Why the Best Technical Decision Is Sometimes the Boring One

Choosing proven, well-understood technology over novel alternatives is not a failure of ambition — it is often the highest-leverage technical decision a team can make, especially under time and risk constraints.

Read more

The Contractor Who Documents Everything Wins. Here Is Why.

Documentation is not a chore to get through after the real work is done. It is a professional differentiator that determines whether clients can trust you with more.

Read more

Reducing API Complexity in Spring Boot — Consolidation, Query Parameters, and the Endpoints Worth Removing

Every endpoint is a permanent contract the moment a client integrates against it. API surface area grows easily and shrinks painfully. Here is how to keep it smaller from the start and how to reduce it when it has already grown.

Read more