The outbox table

About once a month an order existed and nobody heard about it. The order service saved the order, then published order.created to RabbitMQ, and once a month the process died between the two. Rare enough to be mysterious. Frequent enough to ruin a weekend. Two operations, two systems, no common transaction. Whatever order you do them in, you lose. Commit first, then publish: the order exists, the event never leaves. Publish first, then commit: the commit fails, consumers are already busy with an order that does not exist. We had the first variant in production. ...

March 23, 2021 · 2 min · Murat Useinov

Attributes: metadata moves in with the code

#[Route('/orders/{id}', methods: ['GET'])] public function show(int $id): Response Symfony 5.2 accepts this on PHP 8, and the annotation era quietly starts to end. Same shape as the annotation, but now it is language, not a comment. The engine parses it, static analysis sees it, a typo is a compile-time complaint instead of a route that silently does not exist. Doctrine and the validator are heading the same way. Everything that lived in docblocks will move over the next year or two. ...

December 18, 2020 · 2 min · Murat Useinov

Types are for reading, not for the compiler

OrderRepository in a constructor. Click. I am there. That is how I explore a big Symfony project now, and I noticed my main reason for types has changed. Not bug catching anymore. Navigation. Symfony 5.1 is out, PHP 7.4 is everywhere I work, and the IDE knows every caller and every implementation. Compare with the array-passing style we all wrote for years: public function register($data) { // what is in $data? read three call sites to find out } versus ...

June 27, 2020 · 2 min · Murat Useinov

A Redis lock is a promise you cannot fully keep

Two workers, one order, processed twice. Every project gets this day. Someone writes SETNX, calls it a distributed lock, closes the ticket. I want to slow down here, because the ticket is not closed. The small bugs first. A lock needs a TTL, or a crashed worker holds it forever. A lock needs an owner token, or worker A releases the lock of worker B: $token = bin2hex(random_bytes(16)); $ok = $redis->set('lock:order:'.$orderId, $token, ['nx', 'ex' => 30]); And the release must be atomic: compare the token and delete in one Lua script. Check in PHP, delete in a second command, and there is a gap. Something will land in that gap. ...

February 26, 2020 · 2 min · Murat Useinov

Cursor pagination is part of your API contract

Someone inserts a row while the client walks the pages, and the whole window shifts. Page two shows an item the client already saw on page one. Or an item falls between pages and the client never sees it. For a feed this is annoying. For an export or a sync endpoint this is a data loss bug that nobody can reproduce. I used to think keyset pagination is a performance trick. Deep OFFSET is slow, keyset is fast, end of story. Now I think the performance part is the boring half. Offset pagination over a changing dataset lies to the client. That is the interesting half. ...

January 8, 2020 · 2 min · Murat Useinov

One use case, one class

Four hundred lines in a checkout action, half of them inside one try. That is one way a project dies. The other is OrderManager: thirty methods, six injected services, and nobody can say what the class is for, because it is for everything about orders. Both diseases have the same cure and it is embarrassingly simple. One use case, one class, one public method. final class PlaceOrder { public function __construct( OrderRepository $orders, PaymentGateway $payments, EventDispatcher $events ) { ... } public function handle(PlaceOrderCommand $command): OrderId { // the whole story, top to bottom } } The shape answers three questions by itself. What comes in: one command object, a thing you can log, validate, put in a queue. What it needs: the constructor lists dependencies of this use case, not of orders in general, so when PlaceOrder suddenly needs the mailer you see it and can ask why. What comes out: one result. Reading handle() from top to bottom tells the whole story of placing an order. No chapter hidden in a base class or a trait. ...

June 26, 2019 · 2 min · Murat Useinov

Eloquent observers and hidden control flow

Three thousand welcome emails. A colleague ran a user import on one project last month: loop over a CSV, $user->save(), go home. Next morning we found the script had also warmed the search index three thousand times and invalidated cache after every row. Nobody wrote that in the import script. The observers did. Laravel 5.8 came out yesterday, and reading the changelog brought that evening back, so here is the note. ...

February 27, 2019 · 2 min · Murat Useinov

One payment is enough

Charge the card, send the receipt, ack the job. The worker lost its Redis connection between step one and step three. The queue delivered the job again. The customer paid twice. Support learned some new words from him. Retries are not an edge case. Laravel retries failed jobs by design, and you want that, because networks blink. So every job with a side effect must answer one question: what happens if this runs twice. “It will not run twice” is not an answer. It will. ...

January 21, 2019 · 2 min · Murat Useinov

Where the checkout logic goes

Validate input, reserve stock, create the order, charge the card, fire events. Five steps in one checkout action, and the fat model versus fat controller argument offers only two rooms for them. Both rooms are wrong. Put it all in the controller and you cannot run checkout from anywhere except HTTP. No console command, no queue job, no test without the kernel. Put it in the Order model and the model now knows about payments, stock and notifications, a strange set of friends for an Eloquent class. ...

August 13, 2018 · 2 min · Murat Useinov

GDPR: where the user actually lives

The row in users. Then orders, with the delivery address and phone. Then nginx access logs with emails inside GET parameters of an old unsubscribe endpoint. That is how far I got in the first ten minutes of drawing where one user’s data lives on one project, and the map did not fit on one page. May 25 is close and everyone discusses consent banners. The lawyers can have the banners. The engineering part is the right to erasure. “Delete the user” sounds like one DELETE statement until you sit down with a pen. ...

May 9, 2018 · 2 min · Murat Useinov