Project 3 — Small shop: what we are building
Products, images, a cart and a checkout. This is where roles, files and transactions arrive.
- 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.
$ 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.
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 togetherWhy 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
Storageport, that is a second adapter and nothing else changes. - Payments are the obvious next feature, and payment providers get swapped. A
PaymentGatewayport 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.
The endpoints
| Method and path | What it does | Who can call it |
|---|---|---|
GET /v1/products | browse the catalogue | anyone |
GET /v1/products/{id} | one product | anyone |
POST /v1/products | add a product | admin only |
PATCH /v1/products/{id} | edit a product | admin only |
POST /v1/products/{id}/image | upload a product photo | admin only |
GET /v1/images/{id} | serve a photo | anyone |
GET /v1/cart | what is in my cart | logged in |
POST /v1/cart/items | add to cart | logged in |
DELETE /v1/cart/items/{id} | remove from cart | logged in |
POST /v1/checkout | turn the cart into an order | logged in |
GET /v1/orders | my past orders | logged 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
ioactually 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.
- 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.
- 1Decide what should happen if two people check out the last item at the same moment.
- 2Decide whether prices should be stored on the product or copied onto the order line, and why.
- 3List three things that could go wrong if checkout were not a transaction.