When someone buys a subscription, the payment provider handles the money, but the product still has to decide what that person can use. An entitlement is that decision: "user 42 can use Export to PDF and Cloud Sync until 31 March". Entitlements come from the user's plan (for example, Pro includes a fixed set of features) and are valid for the billing period that was paid for.
The payment provider reports changes as webhooks: HTTP requests sent to our service when a subscription is created, renewed or canceled, or when a payment is refunded. Like any real provider, it may deliver an event more than once, deliver events out of order, or retry after failures.
The entitlements service sits between the two. It turns provider events into an up-to-date answer to one question that the rest of the system asks constantly: what is this user allowed to use right now? It also tells other services when the answer changes.
Payment provider ──webhooks──▶ Entitlements service ──EntitlementsChanged──▶ Other services
▲
└── GET /api/v1/users/{id}/entitlements ── Other services
Build a small Symfony service that tracks which features a user is entitled to, based on webhooks from a fictional payment provider.
Time budget: 6–8 hours. Deadline: one week from receiving the task.
- Webhook endpoint.
POST /webhooks/paymentsreceivessubscription.created,subscription.renewed,subscription.canceledandpayment.refundedevents. Requests are signed with HMAC in a header. Requests with an invalid signature must be rejected. - Entitlement model. Plans map to feature sets. A user has a plan's features until the period end date. Refunds and cancellations affect access according to rules you define and briefly document (for example, a refund revokes access immediately, a cancellation keeps access until period end).
- Read API.
GET /api/v1/users/{id}/entitlementsreturns the user's current features and their validity. The endpoint is protected by JWT (include a test key pair). It must be fast, because other services call it on every request. - Notifications. Whenever a user's entitlements change, publish an
EntitlementsChangedmessage reliably. Symfony Messenger is fine. A real broker is not required. - Stack. Use this repository as the starting point: Symfony, PHP and MySQL as already set up here. Everything
must start with a single
make up(see below). - Tests. Unit and integration tests for what you consider most important.
- README. Update this README with how to run the service, your key decisions and trade-offs, what you would do with more time, and where you used AI.
- Optional. A minimal React page showing a user's entitlements and change history.
A JSON file of sample webhook events is provided with this task. Replay it through the webhook endpoint and make sure the resulting state is correct. Treat it as you would real provider traffic.
AI tools are allowed. Disclose where and how you used them in the README. In the follow-up session you will walk through your solution and discuss specific parts of the code, so make sure you understand and can explain every decision in it.
A lean, working solution with clearly explained reasoning. We prefer this over an elaborate one. Avoid complexity the problem doesn't need, and state your trade-offs honestly.
After submission there is a 60-minute session to walk through your architecture, discuss specific code and failure scenarios, and talk through a small change request.
The service is a Symfony 8.1 application (PHP 8.5, Doctrine ORM 3, MySQL 8.4) that runs entirely in Docker.
The only things needed on the host are Docker (with Compose v2) and make.
| Container | Purpose | Host port |
|---|---|---|
php-web |
Apache + PHP serving public/, the project is mounted live |
4444 → http://localhost:4444 |
php-cli |
PHP CLI with Xdebug for console commands, migrations, tests | — |
mysql |
MySQL 8.4, databases web_dev_task_dev (dev) and web_dev_task_dev_test (test) |
33444 |
The source directory is mounted into both PHP containers, so code changes apply without a rebuild.
-
Install dependencies and start the stack:
make install # composer install via the composer:2.11.1 image make up # build images and start mysql, php-cli, php-web
make installmust come first: the project directory is mounted into the containers, so they use the host'svendor/. -
On start,
php-cliapplies the Doctrine migrations to thedevandtestdatabases, then exits. Check that it succeeded withdocker compose logs php-cli. If MySQL was still initialising on the very first start, runmake migrate. -
Open http://localhost:4444. The response
Hey there!means the stack is up.
The database connection is preconfigured in .env
(mysql://root:@mysql:3306/web_dev_task_dev). In the test environment Doctrine appends _test to the database
name, which resolves to web_dev_task_dev_test. Put local overrides in .env.local (git-ignored).
| Target | What it does |
|---|---|
make install |
Installs Composer dependencies into vendor/ |
make composer |
Opens a shell in the Composer container, e.g. for composer require ... |
make up |
Starts the stack in the background |
make down |
Stops the stack and keeps the database volume |
make shell |
Opens bash inside the running php-web container |
make migrate |
Applies Doctrine migrations to the dev and test databases |
make test |
Runs PHPUnit in php-cli with APP_ENV=test |
make stan |
Runs PHPStan |
make cs-check |
Checks code style with PHP-CS-Fixer (dry run, shows the diff) |
make cs-fix |
Fixes code style with PHP-CS-Fixer |
make clean |
Removes vendor/ and var/cache/* |
make destroy |
clean, then removes the containers, the MySQL volume and the locally built images |
-
Console commands run in the CLI container:
docker compose run --rm php-cli bin/console <command>
-
Schema changes go through migrations: change an entity under
src/Entity, generate a migration inmigrations/, then runmake migrate. -
Add packages with
make composer, thencomposer require <package>inside it, so Symfony Flex recipes register bundles and generate their config. -
To reach MySQL from the host, connect to
127.0.0.1:33444asrootwith an empty password. -
Logs are in
var/log/.
PHPUnit, PHPStan and PHP-CS-Fixer are pre-installed as dev dependencies and run inside php-cli:
| Tool | Run with | Configuration |
|---|---|---|
| PHPUnit | make test |
phpunit.dist.xml, bootstrap in tests/bootstrap.php |
| PHPStan | make stan |
phpstan.neon (level 6: bin, public, src, tests) |
| PHP-CS-Fixer | make cs-check / make cs-fix |
.php-cs-fixer.dist.php (@Symfony ruleset) |
Tests live under tests/Application, which is the suite phpunit.dist.xml runs. A smoke test for GET / is
included as a starting point. Tests run with APP_ENV=test against the web_dev_task_dev_test database.
make destroy && make install && make upThis recreates both databases from scratch and re-applies all migrations.