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:
- create the driver in database
- create a user account for the driver in my B2C directory
- send an email to the driver on successfull creation
- 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