Building a Decoupled Authentication Core: User and Token Entities with the Repository Pattern in LearnHub

In the ongoing development of LearnHub, a key area of focus is establishing a robust and maintainable authentication system. A critical step in achieving this involves implementing core User and Token entities, meticulously designed with a clean architecture approach that leverages ports and interfaces. This strategy ensures our authentication domain remains decoupled from specific infrastructure details, enhancing flexibility, testability, and long-term maintainability.

The Core Challenge: Robust Authentication

Authentication systems are fundamental to almost any application, and LearnHub is no exception. They handle sensitive data and critical access control, making their design paramount. A poorly designed authentication layer can lead to tightly coupled code, making it difficult to swap out data stores, introduce new authentication methods, or even write comprehensive unit tests without complex setups. Our goal was to avoid these pitfalls by adopting a clear separation of concerns.

Embracing Clean Architecture with Ports and Interfaces

To address these challenges, we've embraced principles from Clean Architecture, specifically the concept of "Ports and Adapters" (also known as Hexagonal Architecture). In this paradigm, the core domain logic (the "business rules") remains isolated, defining "ports" (interfaces) that describe how it interacts with external systems. Concrete implementations of these ports, called "adapters," handle the actual interaction with databases, external APIs, or other infrastructure components.

This is where the Repository Pattern becomes invaluable. A repository acts as a port, abstracting the data storage mechanism for our domain entities. The domain doesn't care if data lives in a SQL database, a NoSQL store, or an in-memory collection; it only knows how to ask the repository for User or Token objects.

Defining Domain Entities: User and Token

The authentication domain revolves around two primary entities: User and Token. The User entity encapsulates all the core attributes and behaviors of a user within the LearnHub platform, such as ID, username, and email. The Token entity, on the other hand, represents an authentication token associated with a user, critical for session management and API access.

These entities are pure Python objects, free from any database-specific logic or framework dependencies, residing purely within our domain layer.

The Repository Pattern in Practice

Here's how we've implemented the User entity and its corresponding repository interface and a simplified in-memory implementation for demonstration:

# User Entity (Domain Layer)
class User:
    def __init__(self, id: str, username: str, email: str):
        self.id = id
        self.username = username
        self.email = email

    def __repr__(self):
        return f"<User {self.username}>"

This User class is a simple data structure, representing the user concept in our application's domain. It doesn't know about databases or API calls.

# Abstract Repository (Port)
from abc import ABC, abstractmethod
from typing import Optional

class AbstractUserRepository(ABC):
    @abstractmethod
    def get_by_id(self, user_id: str) -> Optional[User]:
        pass

    @abstractmethod
    def add(self, user: User):
        pass

AbstractUserRepository defines the contract for any user repository. Any part of the application that needs to interact with User data will do so through this interface, not a concrete implementation.

# Concrete Repository (Adapter - for illustration)
class InMemoryUserRepository(AbstractUserRepository):
    def __init__(self):
        self._users = {}

    def get_by_id(self, user_id: str) -> Optional[User]:
        return self._users.get(user_id)

    def add(self, user: User):
        self._users[user.id] = user

InMemoryUserRepository is a concrete implementation of the AbstractUserRepository. In a real application, this would be a database-backed repository (e.g., using an ORM or direct SQL), but for testing or simple applications, an in-memory version works well. The key is that it adheres to the AbstractUserRepository interface.

# Application Service (Uses the Port)
class UserService:
    def __init__(self, user_repository: AbstractUserRepository):
        self._user_repo = user_repository

    def create_user(self, id: str, username: str, email: str) -> User:
        user = User(id, username, email)
        self._user_repo.add(user)
        return user

Finally, an UserService demonstrates how an application layer component interacts only with the AbstractUserRepository interface, completely unaware of the underlying data storage mechanism. This makes UserService easy to test with a mock repository, or adaptable to different storage solutions by simply swapping out the AbstractUserRepository implementation.

Benefits of Decoupling

This architectural approach brings significant benefits to LearnHub:

  • Testability: The UserService can be unit-tested in isolation by injecting a mock AbstractUserRepository, without needing a database.
  • Maintainability: Changes to the database schema or ORM technology do not affect the domain or application logic, only the concrete repository implementation.
  • Flexibility: We can easily swap out data storage technologies (e.g., from an in-memory store to a PostgreSQL database) by providing a new AbstractUserRepository implementation.

By carefully defining our User and Token entities and establishing clear ports and interfaces for their persistence, LearnHub's authentication domain is built on a solid foundation, ready for future growth and adaptation while remaining highly testable and maintainable.


Generated with Gitvlg.com

Building a Decoupled Authentication Core: User and Token Entities with the Repository Pattern in LearnHub
JAIME ANDRÉS MONSERRATE VILLA

JAIME ANDRÉS MONSERRATE VILLA

Author

Share: