System Design

System Design for Small Products

A decision framework for designing small products around real workflows, data risk, team capacity, and evidence for adding complexity.

Rizky Romadon profile photoRizky Romadon··10 min read

Sources

Article details

Context for reading and verifying this note.

Updated Jul 20, 202610 min readSystem Design5 sourcesAI-assisted, human-reviewed

Creation context: AI assisted with research, structure, or drafting. Rizky Romadon reviewed the sources, technical claims, examples, and final publishing decision.

The Design Goal Is the Next Product Decision

Small products rarely fail because they did not start with enough infrastructure. They more often become difficult because the architecture makes normal product changes expensive before the product has learned what users need.

My design goal is therefore not “choose the architecture that can scale forever.” It is:

Choose the smallest architecture that protects today's important data and user journeys while preserving an understandable path to the next likely change.

That goal still requires serious engineering. A small product can process payments, handle personal data, send notifications, or integrate with unreliable systems. “Small” describes the product and team stage, not the consequence of every failure.

Begin With a Workflow, Not a Component Diagram

Before choosing services, queues, or caches, I write the core workflow as states and decisions.

For a representative marketplace order:

Prompt
buyer submits order
  -> validate price and availability
  -> create pending order
  -> authorize payment
  -> confirm or reject order
  -> notify merchant
  -> expose status to buyer

This is a representative design exercise, not a claim about one private production implementation.

The workflow exposes the questions architecture must answer:

  • Which state is authoritative?
  • Which step must be synchronous for the user?
  • Which step may complete later?
  • What happens when payment times out?
  • Can a duplicate request create another order?
  • What does the merchant see if notification fails?
  • How can support repair an incorrect state?

A diagram of five services does not answer those questions. A workflow gives the components a reason to exist.

Write the Constraints Before Selecting Technology

I use four groups of constraints.

ConstraintQuestions
ProductWhat must the user complete, and how quickly?
DataWhat cannot be lost, duplicated, exposed, or silently changed?
OperationsWhat must be observable, recoverable, and supportable?
TeamWhat can this team build, deploy, secure, and debug repeatedly?

Team capacity is an architecture constraint, not an embarrassment. A system that theoretically isolates failures but requires skills, on-call coverage, and deployment tooling the team does not have may reduce real reliability.

I also distinguish a hard requirement from a preference. “Payment state cannot be duplicated” is different from “we prefer event-driven architecture.” The former should control the design.

Use a Complexity Ledger

Every new component should pay for itself with a named capability.

ComponentCapability it can buyCost it introduces
QueueBuffering, asynchronous work, retryOrdering, duplicates, backlog, replay
CacheLower latency or dependency loadInvalidation, staleness, another failure mode
Search indexRelevance and specialized queryingSynchronization, mapping, reindexing
Separate serviceIndependent ownership, deployment, or scalingNetwork failure, contracts, distributed data
Read modelQuery-specific performanceEventual consistency and rebuild logic
Feature flagSeparate deployment from exposureMultiple paths and cleanup debt

The ledger prevents one-sided decisions. A queue is not “more scalable” in the abstract. It is useful when delayed processing, load smoothing, or durable handoff is worth duplicate handling, monitoring, and recovery.

My Default Starting Shape

For many small products, my default would be:

  • one deployable application;
  • modules organized around business capabilities;
  • one relational database with clear table ownership;
  • synchronous APIs for immediate user decisions;
  • a simple background-job mechanism for work that can be delayed;
  • object storage for files;
  • structured logs, basic metrics, and error tracking;
  • managed infrastructure where it reduces operational burden.

A representative source layout might be:

Prompt
src/
  catalog/
    application/
    domain/
    infrastructure/
  orders/
    application/
    domain/
    infrastructure/
  payments/
  merchants/
  shared/

The exact folders matter less than the dependency rules. An orders module should not reach into another module's private tables or construct its internal domain objects directly. Cross-module operations should go through an explicit application interface.

This is a modular monolith: one deployment boundary with internal business boundaries. It keeps refactoring cheaper while the domain is still changing.

Boundaries Should Follow Change and Ownership

“A service should do one thing” is too vague to produce good boundaries. Microsoft’s domain-analysis guidance recommends designing around business capabilities with loose coupling and high functional cohesion, while noting that service-boundary evaluation continues as the workload evolves.

I look for several signals:

  • The same business rules change together.
  • The data has one clear authority.
  • The operations share a consistency requirement.
  • One team or role can explain the capability end to end.
  • The capability can expose a small interface without leaking its internal schema.

For the marketplace example, orders and payments may be separate modules because they have different state models and external dependencies. That does not automatically mean they need separate processes or databases on day one.

Decide Synchronous Versus Asynchronous Per Step

I do not classify an entire product as synchronous or event-driven. I classify workflow steps.

Use a synchronous call when the caller needs an immediate decision and the dependency can fit within the deadline. Use asynchronous processing when work may complete later, needs buffering, or should survive a temporary consumer outage.

StepLikely starting modeReason
Validate order requestSynchronousCaller needs immediate correction
Create authoritative orderSynchronous transactionCaller needs a stable reference
Send confirmation emailAsynchronousDelivery can happen later
Update analyticsAsynchronousNot part of order correctness
Authorize paymentRequirement-dependentUser experience and provider contract decide

Asynchronous does not mean “fire and forget.” The design needs durable handoff, idempotent processing, retry limits, backlog visibility, and a dead-letter or repair path.

Let the Database Protect Important Invariants

Application checks alone can race. If two requests can create the same logical order or claim the last unit of inventory, important invariants may need database constraints, conditional writes, locking, or version checks.

I ask:

  • Which record is the source of truth?
  • Which uniqueness rule must survive concurrent requests?
  • What is the transaction boundary?
  • Which state transition is legal from the current version?
  • How is a duplicate command recognized?
  • What audit information is required?
  • How would support repair an incorrect record?

A relational database is a strong default because transactions, constraints, joins, and mature operational tooling solve many early product needs. “Boring storage” is valuable when it makes correctness arguments easier.

Add a Queue Only With an Operating Plan

Before adding a queue, I complete this record:

Prompt
Producer:
Consumer:
Delivery guarantee assumed:
Idempotency key:
Ordering requirement:
Retryable failures:
Maximum attempts and backoff:
Dead-letter behavior:
Backlog and age alerts:
Replay procedure:
Schema ownership:

If these fields cannot be answered, the queue may only move complexity out of the request thread and into operations.

The first background-job implementation can be modest. A database-backed job table may be enough for low volume and one application. A managed broker becomes more attractive when independent consumers, throughput, durability, or operational tooling justify it.

Add a Cache Only After Measuring the Read Path

A cache introduces another answer to the question “what is the current value?” I therefore want evidence before adding it:

  • the query or dependency is actually a latency or capacity bottleneck;
  • the data has an acceptable staleness window;
  • cache keys and tenant boundaries are safe;
  • invalidation or expiry behavior is defined;
  • a cache miss does not overload the source;
  • the application remains correct when the cache is unavailable.

Sometimes an index, bounded query, pagination rule, or precomputed database column solves the measured problem with less operational cost.

Observability Starts With the Core Workflow

Small teams need high-signal observation because they have fewer people available for production archaeology.

For the representative order workflow, I would begin with:

  • order attempts and outcomes;
  • end-to-end latency distribution;
  • payment dependency error and latency;
  • pending-order age;
  • background notification backlog;
  • structured state-transition logs;
  • a trace or correlation identifier;
  • one business signal such as completed orders.

Infrastructure metrics still matter, but the dashboard should first answer whether the product works for users. The method in Building Calm Backend Systems expands this from observation into recovery and ownership.

Design Support and Repair Earlier

Admin and recovery tools are not optional polish when real users create data.

Before launch, I ask how an authorized operator can:

  • search for a workflow by safe identifiers;
  • see its current state and transition history;
  • distinguish pending work from a permanent failure;
  • retry an idempotent operation;
  • correct data through an audited path;
  • disable a risky feature;
  • export evidence without exposing unnecessary personal data.

Building a small, controlled repair path is often more valuable than adding another architectural layer. Manual database edits are fast until they become unaudited production behavior.

When I Would Split a Service

I would consider a separate deployment when evidence shows one or more of these conditions:

  • a capability needs independent availability or scaling;
  • different teams need genuine release autonomy;
  • its dependency or security boundary must be isolated;
  • its runtime or data technology is materially different;
  • deployments of the combined application repeatedly block unrelated work;
  • the domain boundary has remained stable enough to express a durable contract.

Microsoft’s microservices guidance is explicit about the trade-off: services can provide independent deployment, scaling, and fault isolation, but the whole system gains complexity in communication, data consistency, testing, governance, latency, versioning, and operations.

The extraction plan should identify data ownership, API or event contract, migration sequence, compatibility period, observability, and failure behavior. Splitting code without splitting ownership and data often produces a distributed monolith.

Keep an Architecture Decision Record

For important choices, I use a compact record:

Prompt
Decision:
Use a modular monolith and relational database for the first release.
 
Forces:
One team, changing domain, transactional order workflow, modest traffic.
 
Alternatives:
Separate services; serverless functions per capability.
 
Consequences:
Simple deployment and transactions; modules require dependency discipline.
 
Evidence that would trigger review:
Independent team ownership, measured scaling difference, or repeated release coupling.

The last section prevents “temporary” choices from becoming permanent by accident. It also prevents repeated debates with no new evidence.

A Pre-Launch Design Review

Before launch, I want answers to these questions:

  1. Can we draw the main workflow and its states?
  2. Which data is authoritative and which operations enforce invariants?
  3. Where can a duplicate or partial failure occur?
  4. Which steps must be synchronous?
  5. Which delayed work has backlog and replay visibility?
  6. Which component was added, and what measured need pays for its cost?
  7. Can the team observe user impact?
  8. Can an authorized operator contain and repair a bad outcome?
  9. What evidence would make us change the architecture?

If the team can answer these clearly, the design is probably mature enough for the product's current stage—even if the diagram is not impressive.

Closing Principle

System design for a small product is an exercise in preserving clarity while the product learns. Start with workflows, protect data invariants, make boundaries explicit, and charge every new component against a real capability.

The architecture should not simulate the final company. It should help the current team deliver, observe, and safely change the next version.

Related posts

Related posts will appear after more articles are generated.