How to prevent race conditions when counting related objects

Hi! I’m implementing an invite system where users can join groups using invite codes, and currently everything is working. However, I want to ensure the code is robust against race conditions. I’m still learning about concurrency in web apps, so if any of my questions seem basic or misguided, I apologize and would greatly appreciate any resources on the topic!

Here’s the (unabstracted version of the) Invite accept method I’m working on:

def uses_left(self):
    return self.max_uses - self.inviteused_set.count()

@transaction.atomic
def accept(self, user: 'User'):
    # Check if the user already has a `member` attribute (potential membership check).
    if hasattr(user, 'member'):
        raise ValidationError(_('User is already a Member of a Group.'))

    # Lock the `Invite` row with `select_for_update` to prevent concurrent modifications.
    invite = Invite.objects.select_for_update().get(pk=self.pk)

    # Confirm that the invite is still active and hasn’t reached its usage limit.
    if not (self.is_active and self.uses_left() > 0):
        raise ValidationError(_('Invite is not valid.'))

    # Create a `Member` record between the user and the group.
    invite.group.member_set.create(user=user)

    # Create record of invite used/redeemed by the user
    invite.inviteused_set.create(used_by=user)

    # Re-check invite usage count and, if it’s exhausted, set `invite.is_active` to `False` and save it with `update_fields`.
    # Note: is_active=True is used as UniqueConstraint condition for invite code field
    if self.uses_left() == 0:
        invite.is_active = False
        invite.save(update_fields=['is_active'])

Questions

  1. Should I query the database for a user’s membership instead of checking the member attribute? If so, how can I ensure consistency when handling parallel processes that might assign the user to a group during the invite acceptance?

  2. Is using select_for_update() on the Invite row sufficient to prevent race conditions, or should I also lock the InviteUsed table or other related tables to ensure accurate counting and validity?

  3. Would locking additional tables like InviteUsed and Member add necessary protection against race conditions, or would it introduce unnecessary complexity?

  4. How could I test concurrency in Django, to ensure there are no potential race conditions?

  5. Do you have any other suggestions for handling this type of situation?

Thank you!