Dev Skills

Project 3 — Small shop: what we are building

Products, images, a cart and a checkout. This is where roles, files and transactions arrive.

Topic 74 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Explain ports and adapters and why its dependencies point inwards.
  • Compare the layered, by-feature and hexagonal architectures.
  • Explain why authentication and authorisation are separate checks.

What you will build, and what it looks like when done

A small shop API. Admins add products and upload photos. Customers browse, fill a cart and check out. Checkout takes the stock and creates the order together, or does neither. Here is the finished thing in a terminal.

terminal — the finished project
$ curl -s localhost:8080/v1/products
[{"id":1,"name":"Mug","price_cents":900,"stock":12}]
$ curl -s -X POST localhost:8080/v1/cart/items -H "Authorization: Bearer $TOKEN" \
-d '{"product_id":1,"quantity":2}'
$ curl -s -X POST localhost:8080/v1/checkout -H "Authorization: Bearer $TOKEN"
{"id":7,"total_cents":1800,"status":"placed"}

Why a shop is the right third project

The expense tracker had accounts, but everybody was the same kind of user. A shop has two kinds: customers, who browse and buy, and admins, who add products. That difference introduces a new idea.

It also brings two things neither earlier project needed. Files: product images have to be uploaded, stored and served. And money: when somebody checks out, stock must go down and an order must be created, and one must never happen without the other.

The architecture: ports and adapters (hexagonal)

Project 1 grouped code by layer. Project 2 grouped it by feature. This project uses a third shape, common in larger Go codebases: ports and adapters, usually called hexagonal architecture, and closely related to what people call Clean Architecture.

The idea in one sentence: your business rules sit in the middle and depend on nothing; everything that touches the outside world plugs into them. HTTP, Postgres, the filesystem and a payment provider are not the application. They are things the application talks to.

The three words you need

  • Domain — the types and rules that would still be true with no database and no API. Pure Go. Here: internal/core/domain (Product, Order, Cart).
  • Port — an interface the core declares, describing something it needs. A hole in the side of the hexagon. Here: internal/core/port (ProductRepository, Storage).
  • Adapter — a concrete implementation that plugs into a port, or drives the core from outside. Here: internal/adapter/storage/postgres, internal/adapter/handler/http.

The rule that makes it work: dependencies point inwards

The core declares the interfaces. The adapters implement them. So the arrow points from the outside in: the Postgres adapter depends on your core, not the other way around. This is the dependency inversion principle applied to the whole application.

who imports whom
HTTP handler Postgres repo Disk storage
\ | /
\ v /
-------> internal/core/port <------
^
| (interfaces, declared by the core)
internal/core/service
|
internal/core/domain (pure types, imports nothing)
// core/domain imports: nothing of yours
// core/port imports: core/domain
// core/service imports: core/domain, core/port
// adapter/* imports: core/port, core/domain <- inwards
// cmd/api imports: everything, and wires it together
`core/service` never imports an adapter; it only knows the port. That is what lets you swap Postgres for anything else without touching a rule.

Why this project is the right place for it

Hexagonal costs more than the other two layouts: more folders, more interfaces, more indirection. It pays for itself when you expect the outside world to change, and a shop is that case.

  • Images start on local disk and will have to move to S3 once you run more than one instance. With a Storage port, that is a second adapter and nothing else changes.
  • Payments are the obvious next feature, and payment providers get swapped. A PaymentGateway port keeps that decision out of your rules.
  • The rules are worth testing hard. Checkout arithmetic and stock handling can be tested with fake adapters, with no database and no HTTP anywhere.

The three architectures side by side

  • Layered (Project 1) — also called n-tier or the standard layout. Folders are named after the kind of code. Best for one concept, with the lowest cost and interfaces rarely used. It fails once you add features.
  • By feature (Project 2) — also called package-by-feature or DDD-lite. Folders are named after the business concept. Best for several growing concepts; the service that needs an interface declares it. Low cost, but it fails when features start needing each other.
  • Ports and adapters (Project 3) — also called hexagonal, clean or onion. Folders split inside from outside. Best when the outside world will change; the core always declares the interfaces. Highest cost (more indirection), and overkill for a small app.
Watch out: Do not reach for this by default. On a small service it buys you folders and interfaces you will never use, and reviewers will rightly ask why. Project 2's layout is the sensible default; this one is what you graduate to when swapping an external dependency stops being hypothetical.

The endpoints

Method and pathWhat it doesWho can call it
GET /v1/productsbrowse the catalogueanyone
GET /v1/products/{id}one productanyone
POST /v1/productsadd a productadmin only
PATCH /v1/products/{id}edit a productadmin only
POST /v1/products/{id}/imageupload a product photoadmin only
GET /v1/images/{id}serve a photoanyone
GET /v1/cartwhat is in my cartlogged in
POST /v1/cart/itemsadd to cartlogged in
DELETE /v1/cart/items/{id}remove from cartlogged in
POST /v1/checkoutturn the cart into an orderlogged in
GET /v1/ordersmy past orderslogged in

Authentication and authorisation are different

You already have authentication: the middleware from project two proves who somebody is. This project adds authorisation: deciding what that person is allowed to do.

They fail differently, and the difference matters. Failing authentication means "I do not know who you are": a 401. Failing authorisation means "I know who you are, and you may not do this": a 403.

The exception is when confirming a resource exists would itself leak something. Asking for another customer’s order should be a 404, not a 403, because a 403 tells them the order is real.

The steps

  • Step 1 — setup, the schema, and how to store money and stock.
  • Step 2 — products, plus the admin role and a second middleware.
  • Step 3 — uploading a product image, safely. This is the step with the most ways to go wrong.
  • Step 4 — serving images back, and what io actually gives you.
  • Step 5 — the cart.
  • Step 6 — checkout in a transaction, where stock and orders must agree.
  • Step 7 — orders, then the production pass: logging, rate limits, Docker.
Note: There is no payment provider in this project. Taking real card details brings legal requirements that are not a learning exercise. Checkout creates an order and reduces stock, which is where all the interesting Go and SQL is anyway.
The short version
  • Authentication is who you are (401). Authorisation is what you may do (403).
  • A shop needs two things earlier projects did not: files on disk and money that cannot be lost.
  • Use 404 instead of 403 when a 403 would confirm something exists.
  • No real payments. The learning is in the transaction, not the card.
Remember: Roles are not a bigger login. They are a separate question asked after login has already been answered.
Build this before moving on
Write out the endpoint table and mark each row 401, 403 or 404 for the three cases: not logged in, logged in as a customer, logged in as an admin.
Then try these in your own editor
  1. 1Decide what should happen if two people check out the last item at the same moment.
  2. 2Decide whether prices should be stored on the product or copied onto the order line, and why.
  3. 3List three things that could go wrong if checkout were not a transaction.
Official documentation
Check yourselfTake the Project 3 — Small shop test