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 .proto file defines a CatalogService with at least: GetProduct, ListProducts (with pagination — a page size and a cursor/token), and GetSupplier / 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 .proto with grpcio-tools, and the service implements every method.
  • ListProducts enforces 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 paginated products(...) connection (Relay-style edges/node/cursor/pageInfo), and lets a client select a product and its supplier in 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 supplier across a page of products does not trigger N+1: a per-request DataLoader batches the supplier ids into one BatchGetSuppliers gRPC call.
  • The products connection caps first server-side, and the cap is honored end to end (BFF cap and service cap both hold).
  • A gRPC NOT_FOUND from 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 README documents 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 .proto first and generate stubs before writing any resolver — the schema-first discipline from modules 01–02. The BatchGetSuppliers method 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 BatchGetSuppliers RPC. Keep it per-request (in context_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: 100000 and 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 productAdded subscription (module 04) and justify it, or add a second gRPC service and aggregate both in the BFF.

Further reading & sources

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