Skip to content
chiltepin

Generated from: “Explain the shape of the platform to a new backend engineer: services, databases, third parties.

Platform shape

Written by an agent from the skill, validated by chiltepin check, rendered by the renderer — shown as generated, 13 September 2026.

DOCUMENTONBOARDING

Platform shape

The services, databases, and third parties a backend engineer touches in the first month.

The platform is an online store: a storefront, a checkout, and a warehouse that ships parcels. Seven services own the work, and each one that stores state has its own Postgres database. Services talk over HTTP for reads and over Kafka for facts that already happened. That split is the one constraint that shaped everything below: a service never reads another service's tables.

SECTION 01 · Note

Assumptions

Note
Service names, teams, and vendors are the current production setup as of this doc; confirm against the service registry before you depend on one. Every service runs on Kubernetes in one AWS region with no multi-region failover. Payment is captured at checkout through Stripe, tax is quoted by Avalara, and a single carrier (EasyPost) prints labels. Search runs on OpenSearch, not Postgres full-text.

What the platform talks to

Two kinds of people use the platform, and five vendors sit outside it. Each vendor owns one job we chose not to build: money, tax, labels, email, and SMS. A vendor outage degrades one job and never blocks the others, because every vendor call goes through exactly one service.

SECTION 02 · C4 model

System context

C4 · CONTEXT
C4 diagram: 8 elements, 7 relationshipsPERSONShopperBrowses, adds to cart, pays.PERSONWarehouse agentPicks, packs, and marks parcelsshipped from the ops console.SYSTEMStore platformStorefront API, checkout, andfulfilment.EXTStripeCard authorisation and capture.EXTAvalaraSales tax quote per address.EXTEasyPostCarrier rates and shippinglabels.EXTSendGridTransactional email.EXTTwilioSMS delivery alerts.1234567
1browses and buys [HTTPS]2picks and ships [HTTPS]3charges cards [REST]4quotes tax [REST]5buys labels [REST]6sends email [REST]7sends SMS [REST]
LegendPERSONpersonSYSTEMsoftware systemEXTexternal systemoutside the boundaryusesasync / optionalthe system in scope

Services, stores, and the bus

The gateway is the only public entry; it verifies the session token with identity and forwards to one of four request-serving services. The three workers never receive HTTP; they consume Kafka topics and write to their own stores. Postgres is one RDS cluster with one database per service, so a migration in orders_db cannot lock a catalog query.

SECTION 03 · Architecture

Runtime topology

INFRA
Block diagram: 13 nodes, 19 connectionsRequest servicesWorkersData storesEdgeMessaginggatewayKongGATEWAYidentityGoSVC×3catalogGoSVC×3ordersGoSVC×4paymentsGoSVC×2fulfilmentPythonSVC×2notifierNodeSVC×2search-indexerGoSVCPostgresRDS 16 · one databaseper serviceDBRedissessions · pricecacheCACHEOpenSearchproduct indexDBS3labels · invoicesBUCKETKafkaMSK · 3 brokersBUS1234567891011
1verify token2sessions3price cache4query5order.placed6payment.captured7order.placed8all order topics9product.changed10upsert11labels
LegendGATEWAYgatewaySVCserviceDBdatabaseCACHEcacheBUCKETobject storeBUSstream×Nreplicascallsasync / optionalentry point

Each worker keeps its own consumer group and its own offset, so a stalled notifier delays email but never delays a pick. Redis is a cache, not a store: catalog rebuilds a missing price from catalog_db on every miss. Kafka retains seven days, which is the replay window for a worker rebuild.

The request that pays the bills

Checkout crosses four services and one vendor in one synchronous call. The order row is inserted as PENDING before the charge, and it commits as CONFIRMED or PAYMENT_FAILED in the same transaction that records the Stripe result. Every downstream worker starts from order.placed, so a service that never sees that event never sees the order.

SECTION 04 · Sequence
SEQUENCE·POST/v1/orders
Sequence diagram: /v1/orders, 15 messages between 7 actorsStorefrontgatewayorderscatalogpaymentsEXTStripeKafkaALT[card authorised][card declined]1POST /v1/orders2forward with tenant and user claims3GET /prices?skus=…4unit prices and stockinsert order PENDING6POST /charges7PaymentIntent.create8succeeded9captured10order.placed11201 CONFIRMED12card_declined13declined14402 PAYMENT_FAILED15response
Legendcallresponseasyncerrorthe answer the caller getsEXTexternal actorfragment (alt / opt / loop)active
p99: 850 msStripe share of p99: 600 ms

The storefront can treat any 201 as final: the money is captured and the event is on the bus. A 402 leaves a PAYMENT_FAILED row so support can see the attempt, and the shopper can retry with a new card against the same cart.

Who owns what

SECTION 05 · Comparison

Service catalog

ServiceRuntimeOwnsStoreEmitsTeam
gatewayKongrouting, rate limits, token checkPlatform
identityGoaccounts, sessions, rolesidentity_db · Redisuser.createdPlatform
catalogGoproducts, prices, stock countscatalog_db · Redis · OpenSearchproduct.changedCommerce
ordersGocart, order lifecycle, refund requestsorders_dborder.placed · order.cancelledCommerce
paymentsGocharges, refunds, Stripe webhookspayments_dbpayment.captured · payment.refundedCommerce
fulfilmentPythonpick lists, labels, shipment statusfulfilment_db · S3shipment.shipped · shipment.deliveredWarehouse
notifierNodeemail and SMS templates, send lognotifier_dbPlatform
search-indexerGoOpenSearch index buildOpenSearchCommerce

Team is the on-call rotation in PagerDuty. Every service has a README with its topics and its run book.

SECTION 06 · Comparison

Third parties

VendorJobCalled byWhen it is down
Stripecard authorisation, capture, refundspaymentscheckout returns 503; carts are kept
Avalarasales tax quote per addressordersorders falls back to the cached state rate table
EasyPostcarrier rates and label PDFsfulfilmentpicks continue; labels queue until it returns
SendGridtransactional emailnotifieremail retries for 24 h from the send log
TwilioSMS delivery alertsnotifierSMS is dropped after three retries
Datadogmetrics, traces, on-call alertsevery servicealerts stop; PagerDuty heartbeat pages Platform

Stripe is the only vendor on the synchronous checkout path, and its outage is the only one a shopper sees as an error. Every other vendor sits behind a worker or a fallback, so the cost of an outage is delay, not lost orders.

The rule that must not break

SECTION 07 · Note

No cross-service database access

Warning
A service reads and writes only its own database. Data another service owns arrives over an HTTP call to that service or from a Kafka topic that service emits. A pull request that adds a second database URL to a service is rejected in review. The RDS security groups enforce the same rule at the network level.
View the Markdown
```meta
title: Platform shape
subtitle: The services, databases, and third parties a backend engineer touches in the first month.
tag: ONBOARDING
```

The platform is an online store: a storefront, a checkout, and a warehouse
that ships parcels. Seven services own the work, and each one that stores
state has its own Postgres database. Services talk over HTTP for reads and
over Kafka for facts that already happened. That split is the one constraint that shaped everything
below: a service never reads another service's tables.

```callout
tone: note
title: Assumptions
body: "Service names, teams, and vendors are the current production setup as of this doc; confirm against the service registry before you depend on one. Every service runs on Kubernetes in one AWS region with no multi-region failover. Payment is captured at checkout through Stripe, tax is quoted by Avalara, and a single carrier (EasyPost) prints labels. Search runs on OpenSearch, not Postgres full-text."
```

## What the platform talks to

Two kinds of people use the platform, and five vendors sit outside it. Each
vendor owns one job we chose not to build: money, tax, labels, email, and SMS.
A vendor outage degrades one job and never blocks the others, because every
vendor call goes through exactly one service.

```c4
id: context
title: System context
level: context
nodes:
  - { id: shopper, kind: person, name: Shopper, desc: "Browses, adds to cart, pays." }
  - { id: ops, kind: person, name: Warehouse agent, desc: "Picks, packs, and marks parcels shipped from the ops console." }
  - { id: platform, kind: system, name: Store platform, desc: "Storefront API, checkout, and fulfilment." }
  - { id: stripe, kind: external, name: Stripe, desc: "Card authorisation and capture." }
  - { id: avalara, kind: external, name: Avalara, desc: "Sales tax quote per address." }
  - { id: easypost, kind: external, name: EasyPost, desc: "Carrier rates and shipping labels." }
  - { id: sendgrid, kind: external, name: SendGrid, desc: "Transactional email." }
  - { id: twilio, kind: external, name: Twilio, desc: "SMS delivery alerts." }
edges:
  - { from: shopper, to: platform, label: browses and buys, tech: HTTPS }
  - { from: ops, to: platform, label: picks and ships, tech: HTTPS }
  - { from: platform, to: stripe, label: charges cards, tech: REST }
  - { from: platform, to: avalara, label: quotes tax, tech: REST }
  - { from: platform, to: easypost, label: buys labels, tech: REST }
  - { from: platform, to: sendgrid, label: sends email, tech: REST, kind: dashed }
  - { from: platform, to: twilio, label: sends SMS, tech: REST, kind: dashed }
```

## Services, stores, and the bus

The gateway is the only public entry; it verifies the session token with
`identity` and forwards to one of four request-serving services. The three
workers never receive HTTP; they consume Kafka topics and write to their own
stores. Postgres is one RDS cluster with one database per service, so a
migration in `orders_db` cannot lock a `catalog` query.

```block
id: topology
title: Runtime topology
preset: infra
groups:
  - { id: edge, col: 1, row: 1, cols: 1, rows: 3, label: Edge }
  - { id: svc, col: 2, row: 1, cols: 4, rows: 1, label: Request services }
  - { id: workers, col: 2, row: 2, cols: 4, rows: 1, label: Workers }
  - { id: data, col: 2, row: 3, cols: 4, rows: 1, label: Data stores }
  - { id: bus, col: 6, row: 1, cols: 1, rows: 3, label: Messaging }
nodes:
  - { id: gw, col: 1, row: 2, kind: gateway, name: gateway, tech: Kong }
  - { id: identity, col: 2, row: 1, kind: service, name: identity, tech: Go, replicas: 3 }
  - { id: catalog, col: 3, row: 1, kind: service, name: catalog, tech: Go, replicas: 3 }
  - { id: orders, col: 4, row: 1, kind: service, name: orders, tech: Go, replicas: 4 }
  - { id: payments, col: 5, row: 1, kind: service, name: payments, tech: Go, replicas: 2 }
  - { id: fulfilment, col: 2, row: 2, kind: service, name: fulfilment, tech: Python, replicas: 2 }
  - { id: notifier, col: 3, row: 2, kind: service, name: notifier, tech: Node, replicas: 2 }
  - { id: indexer, col: 4, row: 2, kind: service, name: search-indexer, tech: Go, replicas: 1 }
  - { id: pg, col: 2, row: 3, kind: postgres, name: Postgres, tech: "RDS 16 · one database per service" }
  - { id: redis, col: 3, row: 3, kind: redis, name: Redis, tech: "sessions · price cache" }
  - { id: search, col: 4, row: 3, kind: db, name: OpenSearch, tech: "product index" }
  - { id: s3, col: 5, row: 3, kind: s3, name: S3, tech: "labels · invoices" }
  - { id: kafka, col: 6, row: 2, kind: kafka, name: Kafka, tech: "MSK · 3 brokers" }
edges:
  - gw -> identity: verify token
  - gw -> catalog
  - gw -> orders
  - gw -> payments
  - identity -> pg
  - identity -> redis: sessions
  - catalog -> pg
  - catalog -> redis: price cache
  - catalog -> search: query
  - orders -> pg
  - payments -> pg
  - orders -> kafka: order.placed
  - payments -> kafka: payment.captured
  - kafka --> fulfilment: order.placed
  - kafka --> notifier: all order topics
  - kafka --> indexer: product.changed
  - indexer -> search: upsert
  - fulfilment -> pg
  - fulfilment -> s3: labels
```

Each worker keeps its own consumer group and its own offset, so a stalled
`notifier` delays email but never delays a pick. Redis is a cache, not a
store: `catalog` rebuilds a missing price from `catalog_db` on every miss.
Kafka retains seven days, which is the replay window for a worker rebuild.

## The request that pays the bills

Checkout crosses four services and one vendor in one synchronous call. The
order row is inserted as `PENDING` before the charge, and it commits as
`CONFIRMED` or `PAYMENT_FAILED` in the same transaction that records the
Stripe result. Every downstream worker starts from `order.placed`, so a
service that never sees that event never sees the order.

```sequence
id: checkout-path
endpoint: { method: POST, path: /v1/orders, status: 201 }
actors:
  - { id: app, name: Storefront }
  - { id: gw, name: gateway }
  - { id: orders, name: orders }
  - { id: catalog, name: catalog }
  - { id: payments, name: payments }
  - { id: stripe, name: Stripe, external: true }
  - { id: kafka, name: Kafka }
messages:
  - app -> gw: POST /v1/orders
  - gw -> orders: forward with tenant and user claims
  - orders -> catalog: GET /prices?skus=…
  - catalog --> orders: unit prices and stock
  - { from: orders, to: orders, label: "insert order PENDING", kind: note }
  - orders -> payments: POST /charges
  - payments -> stripe: PaymentIntent.create
  - alt: card authorised
  - stripe --> payments: succeeded
  - payments --> orders: captured
  - { from: orders, to: kafka, label: order.placed, kind: async }
  - orders --> gw: 201 CONFIRMED
  - else: card declined
  - { from: stripe, to: payments, label: card_declined, kind: error }
  - payments --> orders: declined
  - orders --> gw: 402 PAYMENT_FAILED
  - end
  - gw --> app: response
foot:
  - { label: p99, value: 850 ms }
  - { label: Stripe share of p99, value: 600 ms }
```

The storefront can treat any `201` as final: the money is captured and the
event is on the bus. A `402` leaves a `PAYMENT_FAILED` row so support can see
the attempt, and the shopper can retry with a new card against the same cart.

## Who owns what

```table
id: service-catalog
title: Service catalog
columns: [Service, Runtime, Owns, Store, Emits, Team]
rows:
  - [gateway, Kong, "routing, rate limits, token check", "—", "—", Platform]
  - [identity, Go, "accounts, sessions, roles", "identity_db · Redis", user.created, Platform]
  - [catalog, Go, "products, prices, stock counts", "catalog_db · Redis · OpenSearch", product.changed, Commerce]
  - [orders, Go, "cart, order lifecycle, refund requests", orders_db, "order.placed · order.cancelled", Commerce]
  - [payments, Go, "charges, refunds, Stripe webhooks", payments_db, "payment.captured · payment.refunded", Commerce]
  - [fulfilment, Python, "pick lists, labels, shipment status", "fulfilment_db · S3", "shipment.shipped · shipment.delivered", Warehouse]
  - [notifier, Node, "email and SMS templates, send log", notifier_db, "—", Platform]
  - [search-indexer, Go, "OpenSearch index build", OpenSearch, "—", Commerce]
note: "Team is the on-call rotation in PagerDuty. Every service has a README with its topics and its run book."
```

```table
id: vendors
title: Third parties
columns: [Vendor, Job, Called by, When it is down]
rows:
  - [Stripe, "card authorisation, capture, refunds", payments, { v: "checkout returns 503; carts are kept", tone: neg }]
  - [Avalara, "sales tax quote per address", orders, { v: "orders falls back to the cached state rate table", tone: warn }]
  - [EasyPost, "carrier rates and label PDFs", fulfilment, { v: "picks continue; labels queue until it returns", tone: warn }]
  - [SendGrid, "transactional email", notifier, { v: "email retries for 24 h from the send log", tone: muted }]
  - [Twilio, "SMS delivery alerts", notifier, { v: "SMS is dropped after three retries", tone: muted }]
  - [Datadog, "metrics, traces, on-call alerts", every service, { v: "alerts stop; PagerDuty heartbeat pages Platform", tone: neg }]
```

Stripe is the only vendor on the synchronous checkout path, and its outage is
the only one a shopper sees as an error. Every other vendor sits behind a
worker or a fallback, so the cost of an outage is delay, not lost orders.

## The rule that must not break

```callout
tone: warn
title: No cross-service database access
body: "A service reads and writes only its own database. Data another service owns arrives over an HTTP call to that service or from a Kafka topic that service emits. A pull request that adds a second database URL to a service is rejected in review. The RDS security groups enforce the same rule at the network level."
```