← Back to the section

When you have a marketplace business brief and need to write the first line of code, there's a temptation to dive straight into the details: what aggregate the order has, what tables go in the database, how the Saga is built. That's a mistake. Before any of that, you need to answer a simpler question: which services will exist in the system at all, and where the boundary between them lies.

Skip this step and you'll end up with code where Catalog calculates commission, Order writes to the catalog, and Payment knows about discounts. Six months later, that's a rewrite from scratch.

This article walks through eight steps: from the brief to a services map with boundaries, integrations, and a failure plan.

Step 1. Collect the domain events

The first step is called Event Storming: take the brief and write down everything that happens in the system, in chronological order. Not "entities" and not "tables" — but events: "the buyer added an item to the cart", "the seller confirmed the order", "the moderator rejected the listing".

The format is simple: each event is a verb in the past tense. "Order created", "payment accepted", "listing approved".

The marketplace brief yields around 50 events. They naturally group into flows:

  • Catalog: seller uploaded a listing → moderator approved it → buyer added it to favorites → listing pulled from sale.
  • Purchase: cart created → item reserved → order paid → order delivered → buyer confirmed.
  • Money: payment accepted → funds in escrow → payout sent to seller → refund requested.
  • Notifications: notify the seller about a new order → notify the buyer about shipment → remind about confirmation.
  • Disputes: dispute opened → operator picked it up → refund.
  • Authorization: buyer logged in → seller passed two-factor verification.

These flows are the future service boundaries. Not every flow becomes a separate service, but every service grows out of one or two flows.

Step 2. Find the context boundaries

From the events we carve out Bounded Contexts — parts of the system, each with its own language. That sounds abstract, so here's a concrete example: the word "order" in the payment part means "a financial document with reserved funds", while in the catalog it means "a row for analytics". Give these two meanings a single name in a single database, and within a year nobody will understand what goes where.

For a marketplace, six contexts suggest themselves:

ContextWhat's inside
CatalogProducts, listings, search, moderation
OrderCart, order, item reservation, statuses
PaymentPayments, escrow, payouts, commission
NotificationChannels (email/sms/push), templates, delivery
CustomerBuyers, sellers, registration, authorization
BackofficeModeration, disputes, operator action audit

These are service candidates. The question "should we do microservices" isn't decided by this step — it only tells you where the natural fault lines are.

Step 3. Decide: monolith or microservices

This is a separate decision that depends on load, deadlines, and team composition. For the marketplace, the inputs are:

  • Load: tens of thousands of read requests per second on the catalog, thousands on checkout, single-digit on payments. Catalog reads need to scale independently.
  • Deadlines: first version in six months, four development teams.
  • Compliance: Payment must be isolated per PCI-DSS requirements.

The decision is microservices per context, with no splitting inside a context:

  • Catalog — separate, because read load and the search team are separate.
  • Order — separate, because reservations and payments can't be lost.
  • Payment — separate for security requirements (the PCI-DSS scope is isolated).
  • Notification — separate for load and integrations with external providers.
  • Customer and Backoffice — each its own service, without significant load.

What we don't do: we don't split Catalog into Search, Pricing, and Reviews for the sake of "correct architecture". The fault lines run along events and contexts, not along technical layers.

Step 4. Draw the map with boundaries

We end up with six services. For each one it matters to fix down not only what it does, but also what it doesn't do — that matters more.

diagram

Each service's constraints:

  • Catalog does not calculate money. The price on a listing is for reference; the real price is fixed in Order at checkout.
  • Order does not write to the catalog. An item reservation is a record in Order, not in Catalog.
  • Payment does not know about orders. Only about payments and payout plans. The link to an order is via an external identifier.
  • Notification decides nothing. It only sends on command. The logic of "when to notify" lives in the event source.
  • Customer does not store the seller's product profile. Customer is identity and authorization, separate from the catalog.
  • Backoffice does not edit data directly — actions go through the API of the corresponding service with a record in the audit log.

These constraints are the most valuable thing on the map. Without them, within a year Catalog will start calculating commission and Order will start writing to the catalog.

Step 5. Determine who owns what

Every entity has exactly one owner. Everyone else reads through integrations and never reaches into someone else's database directly.

EntityOwnerWho reads
Product listingCatalogOrder (snapshot at checkout)
Stock levelCatalogOrder (via reservation)
OrderOrderPayment, Notification, Backoffice
Payment / escrowPaymentOrder (status), Backoffice
Seller payoutPaymentBackoffice
Buyer profileCustomerOrder, Notification
Seller profileCustomerCatalog, Payment, Backoffice
Authorization tokenCustomerEveryone (via JWT)
Notification templateNotification
Action auditBackoffice

A few rules that follow from this:

Snapshot when crossing a boundary. When an Order is placed, the item's price and name are copied into the order. If the seller changes the listing tomorrow, the already-created order does not change. That's correct: the buyer should get what they agreed to.

Foreign identifier, not structure. Order references a product by productId; it doesn't drag the whole listing into itself. It doesn't duplicate someone else's structure.

For reporting — a separate read model. Backoffice reads through an API or through an event stream; it doesn't JOIN across other services' databases.

Step 6. Choose how services communicate

Four interaction options, each for its own case:

Synchronous calls (REST) — when you need an answer right now:

  • Web → Catalog (show a product listing)
  • Web → Order (create an order)
  • Order → Catalog (reserve stock)
  • Web → Customer (log in)

Asynchronous events via Kafka — when a service must react, but there's no need to block the caller:

  • OrderCreated → Notification (notify the seller), Payment (create a payment intent)
  • PaymentCaptured → Order (move to "paid" status)
  • CardModerated → Catalog (publish the listing), Notification (notify the seller)

Saga for the distributed "place order" operation — item reservation (Catalog) + payment (Payment) + order creation (Order). If any step fails, the rest are rolled back. More detail in the Order Service specification.

Transactional Outbox — so an event isn't lost. The event is saved to a separate table in the same transaction as the business data. A separate process periodically reads it and publishes to Kafka. This guarantees the event won't disappear if the service crashes between writing to the database and sending to the queue.

What we don't do:

  • No two-way synchronization. If Catalog changed, Order learns about it via a snapshot or an event, not via "re-query and update your own copy".
  • No shared database. Even at the start, services don't share a PostgreSQL schema.
  • No chains of five synchronous calls. One user request means no more than two synchronous hops between services.

Step 7. Plan behavior under failure

For every synchronous interaction you need a plan: what happens if the neighbor is unavailable.

FailsWho suffersWhat we do
CatalogWeb, OrderWeb shows cached listings. Order refuses checkout — you can't reserve without an up-to-date stock level.
OrderWebWeb shows "checkout unavailable". No payment is created.
PaymentOrderThe Saga puts the payment on hold, retrying for up to 24 hours. After that — order cancellation.
CustomerEveryoneJWT is verified locally against the public key, without calling Customer. Only new logins are unavailable.
NotificationNobody suffers. Events stay in Kafka; notifications arrive later.
BackofficeThe audit accumulates in a queue; the operator handles it later.

The main principle: a Notification or Backoffice outage does not affect purchases. A Catalog or Order outage does, but with graceful degradation — the old cache, refusal only on new orders rather than across the whole site.

These decisions are made before the detailed design of each service. If you design a service without knowing its behavior when a neighbor fails, you'll have to redo error handling and the retry mechanism.

Step 8. Visualize the result

The final artifact is a container-level diagram (C4 Container). It's what you show a new developer and present at a technical walkthrough with the customer.

diagram

Every node on the diagram is a separate process, its own database, its own deployment. What's inside each is described in its own specification.

What's next

After the services map, each service gets a detailed specification at the appropriate level of complexity:

  • Catalog — CQRS: reads through Elasticsearch, writes through PostgreSQL.
  • Order — a ready specification: three aggregates, Saga, Outbox, access control.
  • Payment — strict idempotency, PCI-DSS isolation.
  • Notification — a ready specification: delivery decoupled from the channel.
  • Customer — OAuth2 + JWT, without complex domain logic.
  • Backoffice — CRUD plus audit.

The level of detail is chosen by the complexity of the domain, not for the sake of uniformity. Catalog is read by millions of users — it needs CQRS. Backoffice handles ten requests a minute — aggregates aren't needed there.

In short

  • Start with events, not entities: Event Storming reveals natural groups — the future contexts.
  • A Bounded Context is a part of the system with its own language: the same word means different things in different contexts.
  • The "monolith or microservices" decision is made separately, based on load, team, and requirements.
  • A service boundary is what it doesn't do, not just what it does.
  • Every entity has exactly one owner; the rest read through an API or events.
  • Synchronous calls — when you need an answer now; asynchronous events — when there's no need to block.
  • A Saga is needed for operations that span several services and must roll back on failure.
  • Transactional Outbox guarantees an event won't be lost if the service crashes.
  • Behavior under a neighbor's failure is thought through before detailed design, not after.

Further reading

  • Order Service specification — what a detailed service specification looks like, using Order as an example.
  • Notification Service specification — a simple service at complexity Level 1.
  • Marketplace business brief — the starting point where this walkthrough began.