How to architect the external evnets and Django models?

I'm building a django backend app and I have a few different models in my app. One of these models is a Driver and I want to do the following when calling the create-driver endpoint:

  1. create the driver in database
  2. create a user account for the driver in my B2C directory
  3. send an email to the driver on successfull creation
  4. send a notification to admins on succesfull creation

Operation 1 and 2 should either be succesfull or fail together (an atomic transaction).

I was initially handling 1 and 2 in the Driver model's save method like this:

Class Driver(models.Model):
  #...

  def save(self, *args, **kwargs):
      if self.pk is None:
        # Creating a new driver
          if self.b2c_id is None:
              graph_client = MicrosoftGraph()
              try:
                  with transaction.atomic():
                      user_info = B2CUserInfoSchema(
                          display_name=f"{self.first_name} {self.last_name}",
                          first_name=self.first_name,
                          last_name=self.last_name,
                          email=self.email,
                          phone_number=self.phone,
                          custom_attributes={
                              "Role": UserRole.DRIVER.value,
                          },
                      )
                      res = graph_client.create_user(user_info) # create B2C user
                      self.b2c_id = res.id
                      super().save(*args, **kwargs) # create driver in database
              except Exception as e:
                  raise e
      else:
          # Updating an existing driver
          super().save(*args, **kwargs)```

This was working perfectly fine but I didn't like mixing responsibilites here and adding the B2C user creation logic to my Driver's save method. I like to keep the save method simple and focused on creating a database record.

I tried updating the architecture and started using controllers and event dispatchers to handle this. My architecture is like below now:

class Driver(models.Model):
    # ...

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.domain_events = []

    def save(self, *args, **kwargs):
        if self.pk is None:
            self.domain_events.append(EntityEvent(self, EntityEventType.CREATE))
        else:
            self.domain_events.append(EntityEvent(self, EntityEventType.UPDATE))
        super().save(*args, **kwargs)



class EntityEventType(Enum):
    CREATE = "create"
    UPDATE = "update"


class EntityEvent:
    def __init__(self, db_entity: models.Model, event_type: EntityEventType):
        self.db_entity = db_entity
        self.event_type = event_type


class EntityEventDispatcher:
    def __init__(self, b2c_entity_service: B2CEntityService):
        self._b2c_entity_service = b2c_entity_service

    def dispatch(self, events: list[EntityEvent]):
        for event in events:
            match event.event_type:
                case EntityEventType.CREATE:
                    self._b2c_entity_service.create_entity(event.db_entity)
                case EntityEventType.UPDATE:
                    self._b2c_entity_service.update_entity(event.db_entity)

class EntityController:
    def __init__(self, db_entity: models.Model):
        self._db_entity = db_entity
        self._event_dispatcher = EntityEventDispatcher(
            B2CEntityFactory.from_entity_type(type(db_entity))
        )

    def _dispatch_events(self):
        self._event_dispatcher.dispatch(self._db_entity.domain_events)

    @transaction.atomic
    def create_entity(self):
        self._db_entity.save()
        self._dispatch_events()
        return self._db_entity

    @transaction.atomic
    def update_entity(self):
        self._db_entity.save()
        self._dispatch_events()
        return self._db_entity

    @transaction.atomic
    def delete_entity(self):
        self._db_entity.delete()
        self._dispatch_events()
        return self._db_entity

class DriverController(EntityController):
    def __init__(self, driver: Driver):
        super().__init__(driver)

As you can see, I'm using dispatchers and controllers now and I keep the logic separated. How I'm creating drivers is as below now:

def create_driver(request):
    data = json.loads(request.body)
    driver_controller = DriverController(Driver(**data))
    driver = driver_controller.create_entity()

Obviously, more code was added and a few classes were created in the 2nd approach but the benefit I gained was separating the logic between the database models and external dependencies.

Now I should be able to easily add my external dependencies such as sending emails and notifications by simply adding events in my DriverController and send them to my dispatcher to handle the events.

I like to know whether this is a valid approach or I'm overcomplicating my solution. What are the pros and cons of this approach?

Thanks

Вернуться на верх