This presents a problem, since duplicate inserts are a little difficult to prevent without any explicit blocking.
The first option would be to use a reward or user identifier as a semaphore to efficiently serialize all your records to a specific user or reward identifier. This way you do something like:
SELECT * FROM reward WHERE id = ? FOR UPDATE;
Then each attempt to redeem the same reward will wait for other attempts to pass the first. You can do the same with the user, depending on your approach to traffic. Then, when the transaction completes, the lock is released.
This works by blocking rows in a transaction reward. The main advantage here is that it is simple. The main disadvantage is that only your application is covered by the ability to serialize reading in this way.
Thus, many people can reuse different reward identifiers at once, but for each reward identifier, it blocks a row in the reward table and expects others to do the same until it does.
The second approach is to use advisory locks. There are some advantages here, but there are some disadvantages, so I would look at this (and spend some time on the documentation) if line locks will not do what you need.
Edit:
A unique index is actually possible, but for this you need to rethink your database a bit. As you mentioned, the combination (user_id, reward_id) unique to certain types of rewards.
So, you need to create a unique index in the reward table for reward_id, reward_type , and then add rewards for your foreign key in the redeemed_rewards table to your archived code.
CREATE UNIQUE INDEX redeemed_rewards_only_one_per_user ON redeemed_rewards (user_id, reward_id) WHERE reward_type = 'ONE_PER_USER`
PostgreSQL then throws an error when only these types of rewards are redeemed more than once. This has the advantage that the logic is directly applied in db, so you donβt have to worry about bothering with a wandering request or administrative action.