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 outsideconfig/(chapter 2) — picks upnullonly in production, afterconfig:cache -
.envis not in the repository (chapter 2) — secrets stay in the history and remain traceable after deletion -
APP_DEBUGisfalsein 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_idis returned to keep investigation possible (chapter 9) — you can answer tickets without exposing internals
Database
- No procedure runs
migrate:freshin 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
rollbackit 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:cacheis 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 -
nullableandsometimesare not confused (chapter 5) — a missing key andnullare 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_pagehas an upper bound (chapter 8) —per_page=100000returns everything - Search
LIKEuses 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::beforeis 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
apigroup 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_CONNECTIONis not left atsync(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:restartis 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 -
throwistrue(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:runline is registered with cron (chapter 20) — forget it and nothing happens, with no error - You understand the relationship between
timezoneinconfig/app.phpand 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. Afileorarraycache 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
slackchannel'slevelis lowered (chapter 21) — the default iscritical, soLog::errornever arrives -
LOG_LEVELis not reused across channels (chapter 21) — lowering the file's level floods Slack - Logs rotate (chapter 21) —
singlefills 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
stackhas 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:cacheruns first (chapter 21) — with the previous cache in place,migrateruns against the old connection -
queue:restartcomes 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:linkis placed according to your deployment scheme (chapters 19, 21) — with a directory per release,public/storagedisappears 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.
| Chapter | What it settled |
|---|---|
| 1 | The subject and the endpoints of the finished system |
| 2 | The structure of bootstrap/app.php; env() only inside config/ |
| 3 | Table design, enum casts, #[Fillable], factories |
| 4 | apiResource(), apiPrefix: 'api/v1', route model binding |
| 5 | Fixing the boundary of shape checking with a Form Request |
| 6 | Service classes and the container; separating StockChecker |
| 7 | Deciding the shape of a response with an API Resource |
| 8 | Pagination, filters, and detecting N+1 |
| 9 | Translating exceptions into HTTP; conversions gathered in bootstrap/app.php |
| 10 | Pinning down API behavior with Pest |
| 11 | Sanctum token authentication; carrying guest orders over |
| 12 | Gates and Policies; authorization on the listing API |
| 13 | Custom middleware, webhook signature verification, RateLimiter |
| 14 | Conditional UPDATE, attempts:, a unique constraint for retries |
| 15 | Events and listeners, Notification |
| 16 | Job classes, dispatch, queue:work |
| 17 | Idempotency, retries, failed_jobs, queue:restart |
| 18 | Key design, stampede protection, invalidation |
| 19 | Choosing disks, signed URLs, generating PDFs in a job |
| 20 | routes/console.php, one cron line, auto-cancelling unpaid orders |
| 21 | Channels 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.
| Topic | Where it was stated | Notes |
|---|---|---|
| Blade / Livewire / Inertia | Chapter 1 | Every chapter was about an API returning JSON |
| Broadcasting (Reverb / Echo) | Chapter 1 | Configuring only the server side still needs a receiving client |
| Design theory (DDD tactical patterns, Clean Architecture) | Chapter 1 | Covered in "What to read next" below |
| Docker / server provisioning / CI/CD | The outline | Chapter 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.
What to read next
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.