Skip to content

Global Profile Component

If a person can edit it and it's about them, it belongs to Profile

-- Uncle Bob Martin, probably

Component documentation on Notion ⧉

Key Concepts

Profile

The Profile entity is the main entity of this component. It contains all the personal information of a person. It is composed of several attributes like name, address, phone_number, etc.

Relationships

The main purpose of the Profile is to represent a person. In contrast to User which purpose is to authenticate a person. If your component needs a representation of a person, you should link it to the Profile entity. NOT to the User entity.

A Profile is linked to the User through User.profile_id. The User is a separate entity that contains the authentication information of a person.

  • A User MUST have a Profile
  • A Profile MAY have a User (e.g. kids exists but can't authenticate)

Public API

Profile Service

The ProfileService is the main entry point for any profile related operation. It is responsible for orchestrating the use cases.

For example to change the address of a Profile:

profile_service.change_address(profile_id, street="123 Main St", locality="Springfield", postal_code="62701", administrative_area="IL", country="US")

And to get a Profile:

profile_service.get_profile(user.profile_id)

Events

The component will emit various events following changes in the Profile. You may want to listen to those events to perform additional operations. For example AddressChanged or PhoneNumberChanged.

from shared.messaging.broker import get_message_broker
from components.global_profile.public.events import ContactInformationChanged


def update_phone_number_in_payment_third_party(event: ContactInformationChanged):
    pass


# this is better done in your component's bootstrap
# see https://github.com/alan-eu/alan-apps/blob/main/backend/components/fr/bootstrap/subscriptions.py
message_broker.subscribe(EmailChanged, update_email_address_in_adyen)

DOs and DON'Ts

  • You MUST use the ProfileService to interact with the Profile entity
  • You MUST NOT use Flask Admin to edit Profile data. It would bypass all the business logic and validations. Use the ProfileService instead or any tool that uses ProfileService (e.g. Marmot)
  • You MUST NOT change the Profile model or User's email field directly for the same reason

Internal Structure

[!NOTE] You don't need to read this part if you're not planning to modify the component.

This component tries to adhere to DDD principles, and is structured in the following way

Domain Layer

Directories: internal/domain Goal: All the business logic is encapsulated in this layer and attached to the entities. Contains: The entities, value objects, domain events, exceptions and interfaces for repositories.

For example, the Profile entity has a method change_address that encapsulates the logic for changing the address (value object), this includes some validation and the creation of a domain event.

Infrastructure Layer

Directories: internal/infrastructure and internal/models for the database models (for MoMo compatibility). Goal: All the external dependencies are implemented in this layer. Contains: Repositories, event bus, external services, etc.

For example, the SQLAlchemyProfileRepository implements the ProfileRepository interface. It maps the Profile entity to a ProfileModel SQLAlchemy model to persist the data in the database.

Application Layer

Public

Directories: public Goal: API to interact with the component Contains: The ProfileService class that is the entry point for any profile related operation.

For example, the ProfileService has a method change_address that takes a profile_id and an Address. It converts those into a ChangeAddressCommand and sends it to the MessageBus.

[!WARNING] Calls to the ProfileService methods will commit to database. This is by design to maintain isolation between components.

Private

Directories: internal/application Goal: Orchestrate use cases and handle the communication between the domain and infrastructure layers. Contains: Command, query and domain event handlers.

Events

We distinguish 2 kinds of events

Domain Events

  • Inside the domain, internal to the Profile component itself and used for internal communication only
  • They will be handled inside the same database transaction: all will succeed or all will fail
  • It's just a for loop iterating over hardcoded handlers right before saving to database

Usage examples:

  • Read: Insert data into a table that will be tuned for read purposes (CQRS)
  • Auditability: maintain a record of what changed and when
  • Transactional outbox: insert into an outbox
  • Propagation: let's imagine you've configured your children profiles to use the same address as yours, we could have a handler that would listen to those AddressChanged events on the Profile, load the relationship associated with it (inside global_profile) and cascade the change to the kids

Integration Events

  • They occur once we've committed the transaction and we're sure that the thing actually occurred
  • They are used to communicate outside our own component

Usage examples:

  • When phone number is changed, we want to propagate this to a 3rd party travel insurance
  • When an address is changed, we want the insurance cards that are pending shipping to be updated to the new address

You can read more about this distinction in this article ⧉ and find different implementations in python here ⧉ or there ⧉.

Inversion of Control

Below is a list of concepts and their dependencies in the component. For simplicity, only the main implementation is described for each concept.

Service

Role: The service layer is responsible for orchestrating the use cases. It is the entry point for any operation related to the component. Dependencies: MessageBus and UnitOfWork

Message Bus

Role: The message bus is responsible for dispatching the commands, queries and events to the appropriate handlers inside the component. Dependencies: UnitOfWork and handler functions

Unit of Work

Role: The unit of work is responsible for keeping track of all the changes that have been made to the database. It is responsible for committing or rolling back the transaction as well as dispatching the events when successful. Dependencies: Repository and EventDispatcher

Repository

Role: The repository is responsible for persisting the entities. Dependencies: Session or SessionMaker

Event Dispatcher

Role: The event dispatcher is responsible for dispatching the events outside the component. Dependencies: shared.messaging

Diagram

Note: this is not automatically generated, but a representation of the overall component's structure.

classDiagram
    Service --* MessageBus
    Service --* UnitOfWork
    SQLAlchemyUnitOfWork --|> UnitOfWork
    SQLAlchemyUnitOfWork --* SQLAlchemyProfileRepository
    InMemoryUnitOfWork --|> UnitOfWork
    InMemoryUnitOfWork --* InMemoryProfileRepository
    MessageBus --* UnitOfWork
    UnitOfWork --* Repository
    UnitOfWork --* EventDispatcher
    SQLAlchemyProfileRepository --|> Repository
    InMemoryProfileRepository --|> Repository
    AlanMessagingEventDispatcher --|> EventDispatcher
    TestEventDispatcher --|> EventDispatcher

    class Service {
        +MessageBus message_bus
        +UnitOfWork unit_of_work
    }
    class MessageBus {
        +handle(domainevent | command)
        +collect_processed()
    }
    class UnitOfWork {
        *ProfileRepository profiles
        *commit()
        *dispatch()
        *rollback()
        *flush_events()
    }
    class SQLAlchemyUnitOfWork{
        +SQLAlchemyProfileRepository profiles
    }
    class InMemoryUnitOfWork{
        +InMemoryProfileRepository profiles
    }
    class Repository {
        *persist(entity)
        *flush_events()
    }
    class SQLAlchemyProfileRepository {
        +Session session
    }
    class InMemoryProfileRepository {
        +dict memory
    }
    class EventDispatcher {
        *dispatch external events
    }
    class AlanMessagingEventDispatcher {
        +Broker broker
    }
    class TestEventDispatcher {
        +list dispatched_events
    }
Hold "Alt" / "Option" to enable pan & zoom