Generated from: “Explain the shape of the platform to a new backend engineer: services, databases, third parties.”
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."
```