Skip to main content

Summary — a pre-release checklist

Since chapter 1 raised "the gap between code that works and code you can operate," we have spent 21 chapters growing a single order API.

To finish, here is one list to run through before shipping to production. Each item carries "what happens if you skip it" and the chapter it comes from.

How to use this list​

You do not have to satisfy all of it at once. The level you need varies with the amounts of money and the number of users involved.

Instead, when you skip a check, skip it knowing what happens. That is what the "what happens if you skip it" notes are for. If you read one and can conclude "that will not happen to us," then it is a correct omission.

The chapter numbers are where the reasoning lives. When an item does not make sense, go back to that chapter; the "chapter index" below has links. One line cannot carry the reasons behind a decision.

Configuration and secrets​

  • env() is never called outside config/ (chapter 2) — picks up null only in production, after config:cache
  • .env is not in the repository (chapter 2) — secrets stay in the history and remain traceable after deletion
  • APP_DEBUG is false in production (chapters 2, 9) — stack traces and environment variables appear in responses
  • The 500 body carries no exception message (chapter 9) — table names and file paths leak
  • An error_id is returned to keep investigation possible (chapter 9) — you can answer tickets without exposing internals

Database​

  • No procedure runs migrate:fresh in production (chapter 3) — every table is dropped
  • No procedure runs seeders in production (chapter 3) — test data lands in production
  • Migrations are designed to move forward only (chapter 3) — by the time you need rollback it is usually already too late
  • Foreign key delete cascades are deliberate (chapter 3) — deleting a product deletes past order lines
  • Stock updates use a conditional UPDATE (chapter 14) — concurrent orders drive stock negative
  • Transactions carry attempts: (chapter 14) — a deadlock drops one order
  • No external API calls or mail sends inside a transaction (chapters 14, 15) — a retry executes them twice
  • Retry keys have an expiry (chapter 14) — keys grow without bound
  • Negative stock is checked periodically (chapters 14, 20) — an adjustment from an admin screen breaks it quietly

Routing, input, and output​

  • You have decided whether to expose sequential IDs in URLs (chapter 4) — sequential IDs reveal totals and growth rate from outside
  • Nested resources carry scopeBindings() (chapter 4) — someone else's child resources can be fetched
  • route:cache is used in production only (chapter 4) — locally, changes stop taking effect and cause confusion
  • Only data that passed validation is used (chapter 5) — $request->all() contains unvalidated values
  • nullable and sometimes are not confused (chapter 5) — a missing key and null are different states
  • The 422 response shape is settled (chapter 5) — it cannot change after publishing
  • No queries are issued inside a Resource (chapter 7) — it becomes N+1 in a listing
  • You have decided whether to expose internal IDs in responses (chapter 7) — once exposed, they cannot be removed
  • The date format is settled (chapter 7) — changing it midway breaks clients

Where design lives​

  • No singleton holds request-specific state (chapter 6) — it leaks into the next request
  • Service classes arrive by constructor injection (chapter 6) — calling facades directly is hard to swap in tests
  • The cohesion of extracted classes is checked (chapter 6) — a split alone leaves loosely related methods together

The listing API​

  • per_page has an upper bound (chapter 8) — per_page=100000 returns everything
  • Search LIKE uses a prefix match so an index applies (chapter 8) — a leading wildcard scans the whole table
  • The relationships included in a listing are narrowed (chapter 8) — each record gets expensive to fetch
  • The sort order is deterministic (chapter 8) — rows with equal values duplicate or disappear across pages

Authentication and authorization​

  • The plain-text token is shown only at issue time (chapter 11) — the database holds only a hash
  • Tokens are never logged (chapters 11, 21) — everyone who can read the logs can impersonate
  • Login attempts are rate limited (chapter 13) — brute forcing gets through. At chapter 11 the hole was still open
  • Authorization applies to the listing API (chapter 12) — unlike a single-record leak, everything leaks
  • Gate::before is not used casually (chapter 12) — the administrator check bypasses every Policy
  • A Resource is not substituted for authorization (chapter 12) — hiding fields still leaves the fetch succeeding
  • The authorization decision is not left to the frontend (chapter 12) — calling the API directly gets through

Traffic control​

  • You have listed your public endpoints and decided whether each needs a limit (chapter 13) — the api group has no rate limiting by default, and only routes you mark are limited
  • There are countermeasures against ID enumeration (chapter 13) — ordinary rate limiting is not enough
  • Event types you do not handle also return 200 (chapter 13) — a 5xx makes the sender retry repeatedly
  • Webhook signatures are verified (chapter 13) — anyone can send a payment success
  • The number of 429s is logged (chapters 13, 21) — you cannot notice legitimate users being rejected

Asynchronous work​

  • A listener failure does not take down the main processing (chapter 15) — a failed mail send erases the order
  • Notification recipients are checked (chapter 15) — test mail from development flies to users
  • QUEUE_CONNECTION is not left at sync (chapter 16) — what you thought was queued runs synchronously
  • You notice when the worker dies (chapters 16, 21) — jobs pile up with nobody processing them
  • Something looks at failed_jobs (chapters 16, 17) — it is recorded but nobody is looking
  • Jobs are idempotent (chapter 17) — a retry charges or sends twice
  • The timeout is shorter than retry_after (chapter 17) — the same job runs twice
  • No single job is too big (chapter 17) — a mid-way failure means redoing everything
  • queue:restart is in the deployment procedure (chapters 17, 21) — old code keeps running

Caching​

  • You have checked what happens when the cache goes down (chapter 18) — every request floods the database and takes it with it
  • No invalidation is missing (chapter 18) — it quietly keeps returning stale values
  • Not everything is cached (chapter 18) — you only add consistency problems without getting faster

Files​

  • The disk name is never omitted (chapter 19) — a changed default moves private files to the public side
  • Nothing is written directly into public/ (chapter 19) — it disappears on deployment and cannot move to S3
  • throw is true (chapter 19) — a failed write is only visible in the return value
  • Uploaded filenames are not used as given (chapter 19) — the name the sender supplied cannot be trusted
  • Signed URLs have a short expiry (chapter 19) — authorization is checked only once, at issue time
  • Something sweeps up orphaned files (chapters 19, 20) — a transaction does not roll back files

Scheduled work​

  • One schedule:run line is registered with cron (chapter 20) — forget it and nothing happens, with no error
  • You understand the relationship between timezone in config/app.php and the server clock (chapter 20) — changing only one causes drift
  • onOneServer() is in place with a shared cache before scaling out (chapter 20) — otherwise the same work runs on every server. A file or array cache prevents nothing
  • withoutOverlapping()'s lock expiry is stated (chapter 20) — the 24-hour default is far too long for short intervals
  • You notice when the scheduler has stopped (chapters 20, 21) — it quietly stops doing anything

Logging and monitoring​

  • The slack channel's level is lowered (chapter 21) — the default is critical, so Log::error never arrives
  • LOG_LEVEL is not reused across channels (chapter 21) — lowering the file's level floods Slack
  • Logs rotate (chapter 21) — single fills the disk
  • A request ID flows through the logs (chapter 21) — concurrent logs interleave and cannot be followed
  • The sensitive-data processor is on every channel (chapter 21) — writing it on stack has no effect
  • Expected errors are not mixed into error (chapter 21) — the exceptions you actually want to investigate get buried
  • Health check endpoints sit outside authentication and rate limiting (chapter 21) — otherwise the monitoring service has to hold a token

Tests​

  • There are authorization tests (chapter 10) — a bug exposing other people's data goes unnoticed without one
  • External communication is kept out of tests (chapter 10) — a service outage on their side fails your tests
  • Tests do not depend on execution order (chapter 10) — they fail when run individually
  • Failing tests are not deleted to go green (chapter 10) — the moment you delete one, that contract is gone
  • Every piece of wiring you add has a test that fails when it is removed (chapters 19, 20) — job dispatch and schedule registration can be deleted with everything else still passing

The deployment procedure​

The steps laid out in chapter 21, split into every deployment and once when building an environment.

# Every time
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan queue:restart
  • On a deployment that changes the DB connection in .env, config:cache runs first (chapter 21) — with the previous cache in place, migrate runs against the old connection
  • queue:restart comes after the caches are updated (chapter 21) — run it first and the restarted workers read the old configuration

Two things go in when building the environment.

php artisan storage:link
# one line in crontab
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
  • storage:link is placed according to your deployment scheme (chapters 19, 21) — with a directory per release, public/storage disappears every time, so it belongs in every deployment

Chapter index​

A list of what each chapter settled. Use it as an entry point when coming back.

ChapterWhat it settled
1The subject and the endpoints of the finished system
2The structure of bootstrap/app.php; env() only inside config/
3Table design, enum casts, #[Fillable], factories
4apiResource(), apiPrefix: 'api/v1', route model binding
5Fixing the boundary of shape checking with a Form Request
6Service classes and the container; separating StockChecker
7Deciding the shape of a response with an API Resource
8Pagination, filters, and detecting N+1
9Translating exceptions into HTTP; conversions gathered in bootstrap/app.php
10Pinning down API behavior with Pest
11Sanctum token authentication; carrying guest orders over
12Gates and Policies; authorization on the listing API
13Custom middleware, webhook signature verification, RateLimiter
14Conditional UPDATE, attempts:, a unique constraint for retries
15Events and listeners, Notification
16Job classes, dispatch, queue:work
17Idempotency, retries, failed_jobs, queue:restart
18Key design, stampede protection, invalidation
19Choosing disks, signed URLs, generating PDFs in a job
20routes/console.php, one cron line, auto-cancelling unpaid orders
21Channels and stack, Context, masking, the deployment procedure

What this series did not cover​

Here again are the items placed out of scope in chapter 1 and the outline. It does not mean they are unnecessary — only that they are outside this series' reach.

TopicWhere it was statedNotes
Blade / Livewire / InertiaChapter 1Every chapter was about an API returning JSON
Broadcasting (Reverb / Echo)Chapter 1Configuring only the server side still needs a receiving client
Design theory (DDD tactical patterns, Clean Architecture)Chapter 1Covered in "What to read next" below
Docker / server provisioning / CI/CDThe outlineChapter 21 was limited to configuration on the Laravel side

Scripting the deployment procedure to run automatically sits outside this series. Chapters up to 21 settled what to run, so how to automate that is the next conversation.

This series covered how to write things in the standard structure. The following three extend it in different directions.

  • A Practical Guide to Laravel × DDD × Clean Architecture — rebuilds the same features around which layer things belong in. The service class extracted in chapter 6 splits into value objects, entities, and aggregates
  • PHP Class Design Guide — covers the criteria inside a single class, such as encapsulation and single responsibility. It is the foundation for what chapter 6 called "checking cohesion"
  • Database Design Guide — normalization, E-R diagrams, index design. The theory chapter 3 assumed when building the tables

If you read this series again from the start, the index above is the entry point. You do not have to read all 21 chapters end to end. Open only the chapter for whatever you got stuck on while working.