How do you correctly enforce only one active row per user with Doctrine and PostgreSQL under concurrent requests?

1 day ago 2
ARTICLE AD BOX

I ran into a design problem in a Symfony + Doctrine + PostgreSQL project and I want to make sure I am solving it at the correct layer.

I have an entity where one user can have many rows, but only one of them is allowed to be active at a time.

For example, a user can have multiple records, but only one row with isActive = true.

At first I thought about checking this in application code before flush, but that does not feel reliable once two requests hit at nearly the same time. In that case, both requests could pass the check before either one commits.

Because of that, I am thinking the real solution should be at the database level with a partial unique index on user_id where is_active = true.

The part I am unsure about is how to handle this properly from Doctrine.

My questions are:

Is the partial unique index the correct and safest solution for this kind of rule?

Under concurrent requests, is the right approach to let PostgreSQL reject the second write and then catch the constraint violation after flush()?

After a failed flush caused by that constraint, what is the cleanest way to recover in Doctrine? Is rolling back the transaction enough, or do I need to clear/reset the EntityManager as well?

Is there any cleaner Doctrine-native pattern for this, or is database enforcement the only real answer here?

I am not looking for single-request validation only. My main concern is correctness under race conditions.

Example entity:

#[ORM\Entity] class Subscription { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false)] private ?User $user = null; #[ORM\Column(type: 'boolean')] private bool $isActive = false; }

And the PostgreSQL index I am considering is:

CREATE UNIQUE INDEX uniq_active_subscription_per_user ON subscription (user_id) WHERE is_active = true;

I would appreciate guidance from people who handled this in real projects, especially around Doctrine behavior after the failed flush.

Read Entire Article