Module 12: Capstone Project
Why this matters
Every module in this track has been building toward one picture. Module 00 drew it: a mature system is multi-paradigm, and the recurring miniature of that end state is an internal gRPC service with a GraphQL BFF in front of it for a web client. You learned to define and call gRPC services (modules 01–02), to build an N+1-safe, paginated, authorized GraphQL API (modules 03–04), to choose update mechanisms (module 05) and edge components (module 06), and to make several paradigms coexist and evolve without forking your domain (module 07). This capstone is where you assemble that knowledge into something that runs.
The point isn't to build something large — it's to feel the seam where two
paradigms meet. The BFF speaks GraphQL to the web client on the outside and gRPC
to your service on the inside; it's the translation layer from module 06, the
place over-/under-fetch gets resolved (module 00) and where internal shapes are
mapped to stable external ones (module 07). Building this end to end forces every
concept to become concrete: a .proto contract, generated stubs, a resolver that
calls a gRPC stub, a DataLoader that batches those calls, a schema the client
queries by picking fields. If you can build this, you can reason about — and
build — the real thing.
Keep the whole track's discipline as you go: don't gold-plate. This is deliberately one service and one BFF. No federation, no gateway, no subscriptions unless a requirement genuinely calls for one — add complexity only where a concrete requirement below demands it (module 00).
The project
Build a small catalog system in the multi-paradigm shape: an internal gRPC
catalog-service that owns the data, and a GraphQL BFF in front of it that a
web client queries. Pick any simple domain you like (books, products, movies —
the examples throughout the track used books/orders); the shape matters, not the
subject.
The system has two related types so the BFF has something to aggregate and a real
N+1 risk to defend against — e.g. Product and its Supplier (each
product has one supplier; a supplier has many products).
web client GraphQL BFF (owns NO data) internal service
────────── ────────────────────────── ─────────────────
query { product/products resolvers ── gRPC GetProduct/ListProducts ─►
products(first:20){ ──► maps gRPC msgs -> GraphQL types catalog-
node{ name per-request DataLoader service
supplier{name}}} batches supplier ids ────── gRPC BatchGetSuppliers ─► (data)
} (one query, TWO RPCs, no N+1) ◄──────────────────────
GraphQL over HTTP │ the SEAM: GraphQL outside, gRPC inside │ Protobuf/HTTP2
Work in this order (it mirrors the track): define the gRPC contract, implement
the service, then build the GraphQL BFF that calls it, then harden the BFF. Use
Strawberry + FastAPI for the BFF and grpcio/grpcio-tools for the service, all
running locally (Docker or two processes).
Acceptance checklist
Your system is done when all of these hold:
The internal gRPC service
- A
.protofile defines aCatalogServicewith at least:GetProduct,ListProducts(with pagination — a page size and a cursor/token), andGetSupplier/BatchGetSuppliers(a method that takes many supplier ids and returns them — this is what makes BFF-side batching possible). - Python stubs are generated from the
.protowithgrpcio-tools, and the service implements every method. -
ListProductsenforces a server-side page-size cap (never trust a client-supplied limit). - Domain errors map to correct gRPC status codes (unknown id →
NOT_FOUND, bad argument →INVALID_ARGUMENT). - Business rules (validation) live in the service, not smeared into the transport handler.
The GraphQL BFF
- A Strawberry schema exposes
product(id), a paginatedproducts(...)connection (Relay-styleedges/node/cursor/pageInfo), and lets a client select a product and itssupplierin one query. - Every resolver calls the gRPC service via a stub — the BFF owns no database; it aggregates and shapes what the service returns.
- The BFF maps internal gRPC message shapes to client-facing GraphQL types (don't expose raw generated messages — the module-07 boundary).
- A query selecting
supplieracross a page of products does not trigger N+1: a per-request DataLoader batches the supplier ids into oneBatchGetSuppliersgRPC call. - The
productsconnection capsfirstserver-side, and the cap is honored end to end (BFF cap and service cap both hold). - A gRPC
NOT_FOUNDfrom the service surfaces to the client as a sensible GraphQL error (not a 500 / raw stack trace).
System-level
- The gRPC contract and the GraphQL schema are separate contracts — you can rename an internal gRPC field and, because the BFF maps shapes, the GraphQL client contract is unaffected (demonstrate this).
- A short
READMEdocuments which paradigm serves which consumer and why (the module-07 orientation doc, in miniature). - The web client (or a GraphiQL session standing in for it) fetches a products page and each product's supplier name in one query with two data-source round trips total (one products call + one batched suppliers call), proving the BFF collapsed the fan-out.
Hints
- Start at the contract. Write the
.protofirst and generate stubs before writing any resolver — the schema-first discipline from modules 01–02. TheBatchGetSuppliersmethod exists specifically so the DataLoader has a batch endpoint to call; design it in from the start. - The DataLoader batches gRPC calls, not SQL. This is module 03's N+1 defense
pointed at a different data source: the loader's batch function takes a list of
supplier ids and issues one
BatchGetSuppliersRPC. Keep it per-request (incontext_getter), or you'll leak data across requests (module 03). - The BFF is a translation layer, not a database. Every resolver ends in a gRPC call. If you find yourself adding a DB to the BFF, step back — the service owns the data; the BFF shapes it (module 06).
- Map errors at the seam. Catch gRPC errors in resolvers and translate status
codes to GraphQL errors (
NOT_FOUND→ a clean "not found" error), exactly as module 07's adapter pattern showed — one domain error, faithfully translated per paradigm. - Prove the wins, don't assume them. Log the gRPC calls. A products-page query
selecting supplier should show exactly two RPCs — if you see one-per-product,
your DataLoader isn't wired into the resolver. Request
first: 100000and confirm the cap holds. - Resist scope creep. No gateway (one service, one client), no federation (one
team owns the whole graph), no subscriptions unless you add a live requirement
— each would be premature complexity here (module 00). If you want a stretch
goal, add a
productAddedsubscription (module 04) and justify it, or add a second gRPC service and aggregate both in the BFF.
Further reading & sources
- gRPC Python — Basics tutorial - the end-to-end reference for the
.protocontract, generated stubs, and service implementation the capstone's internal service is built on. - Strawberry — GraphQL over FastAPI - mounting the GraphQL BFF as a FastAPI route with a per-request context for loaders.
- Strawberry — DataLoaders guide - the per-request batching pattern that turns the products-plus-supplier query into two RPCs instead of N+1.
- Relay — GraphQL Cursor Connections specification - the
edges/node/cursor/pageInfoshape required by the paginatedproductsconnection. - Sam Newman — Backends For Frontends pattern - the BFF role the GraphQL layer plays as the translation seam in front of the gRPC service.
- Martin Fowler — Ports and Adapters (Hexagonal Architecture) - the "one domain, many adapters" discipline that keeps business rules in the service, not the BFF.
Next
You've reached the end of track 11 — Advanced API Paradigms. You can now look at any consumer and choose a paradigm deliberately (REST, gRPC, GraphQL, or events), build each one, choose how to keep consumers updated, place the right edge components in front, make them coexist and evolve without breaking, and — as of this capstone — assemble the canonical internal-gRPC-plus-GraphQL-BFF shape end to end.
From here, the natural next step is track 12 — Testing and Code Quality, a shift from designing and exposing APIs to proving they work: unit, integration, and end-to-end tests with real TDD discipline, CI automation, and objective code-quality measurement. The multi-paradigm system you just built — a gRPC service behind a GraphQL BFF, with contracts that must evolve without breaking — is exactly the kind of surface that testing discipline exists to keep honest as it grows.
12-testing-and-code-quality — start the next track.
Back to the track index: ../README.md · Master index: ../../README.md