DEV Community

Cover image for Building SaaS with DDD & Clean Architecture in Python — Issue 1
Sanchit Rk
Sanchit Rk

Posted on

Building SaaS with DDD & Clean Architecture in Python — Issue 1

It's been a few weeks, and I decided to work on a SaaS application — I had a bunch of ideas for some time now and was looking to focus on a problem space worth solving.

I chose Python as the programming language, as I felt a bit rusty — I have been a Node.js developer for a while and wanted to brush up on my Python skills.

Now, I don’t recommend this approach. I suggest focusing on the problem and solution and most importantly talking to customers.

When building real apps, you can select a programming language that you are most familiar with. Focusing on your knowledge and expertise is more important than getting caught up in the complexities of learning or refreshing a new programming language.

My goal was also to brush up on my Python skills and learn DDD and Clean Architecture practices. Hence, I chose Python.

The Problem Space

Companies are using Slack for all internal communications, and with shared channels, outside members can also be part of the conversations. Being used to support customer queries, talk to customers, and collaborate with team members, and much more.
Managing conversations with customers and internal teams can be challenging, as it’s easy to get overwhelmed. To address this issue, I decided to build a SaaS application that seamlessly integrates with Slack and offers effective conversational support for teams and companies. This solution aims to streamline communication processes and enhance efficiency in providing support.

In DDD, the first step is to understand the problem space. Now that we have some understanding of the problem that we are solving, we can focus on the solution space — the implementation details.

Uncovering Entities & Properties

Entity is a domain object when we care about its individuality when distinguishing it from all the other objects in a system. It's a unique thing capable of being changed in its lifetime; changes can be so extensive that the object might seem different, yet it is the same object by identity.

Tenant
As we are building a SaaS, we want to represent our customers. It's typical to call them Tenants. Tenants can have users, customers, billing information, etc. We only have a little information on the Tenant; let's move on. We can add intrinsic details later.

class Tenant(AbstractEntity):
    def __init__(self, tenant_id: str | None) -> None:
        self.tenant_id = tenant_id

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Tenant):
            return False
        return self.tenant_id == other.tenant_id

    def __repr__(self) -> str:
        return f"""Tenant(
            tenant_id={self.tenant_id}
        )"""
Enter fullscreen mode Exit fullscreen mode

We will add more details as we build along; now, the entity looks fine for a start.

User
Tenants will have Users, and I can think of Users to have some role. We only have a little information on how Users will register or get created, but I know how to represent them as an entity. With these details, let's define the User entity.

class UserRole(Enum):
    OWNER = "owner"
    ADMINISTRATOR = "administrator"
    MEMBER = "member"

class User(AbstractEntity):
    def __init__(
        self,
        tenant_id: str,
        user_id: str | None,
        name: str | None,
        role: UserRole.MEMBER,
    ) -> None:
        self.tenant_id = tenant_id
        self.user_id = user_id
        self.name = name
        self.role = role

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, User):
            return False
        return (self.tenant_id == other.tenant_id) \
          and (self.user_id == other.user_id)

    def __repr__(self) -> str:
        return f"""User(
            tenant_id={self.tenant_id},
            user_id={self.user_id},
            name={self.name},
            role={self.role.value}
        )"""
Enter fullscreen mode Exit fullscreen mode

We know that the User should be uniquely identifiable in the Tenant; hence, the equality implementation for the entity object checks for tenant_id and user_id properties.

Understanding __eq__ — in Python is out of the scope of this discussion. I will be writing about it in the future.

Validation
We can safely add some validation in the entity, for example, the User entity — making sure we have the tenant_id — When creating/initializing the User entity, e.g.

...
def __init__(
        self,
        tenant_id: str,
        user_id: str | None,
        name: str | None,
        role: UserRole.MEMBER,
    ) -> None:
        self.tenant_id = tenant_id
        self.user_id = user_id
        self.name = name
        self.role = role
        ...

        assert tenant_id is not None
        assert role is not None
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, I'm making sure we don't set the tenant_id as None/Null — enforcing the domain contract that the User is always part of the Tenant.

For now, I will skip these validations, but there is no harm in having those checks in the domain entity.

In the next issue, #2, I will focus on mapping Slack events APIs to entities and value objects. I will also explore value objects in more detail and intrinsic details on entities vs. value objects.

Till then, follow this series.

Top comments (0)