2
\$\begingroup\$

I'm trying to understand the DDD architecture pattern. I wrote a simple project in which I tried to use DDD arch. Here are my doubts after implementing it:

  1. Does it make sense to use an ORM for a DDD project? Maybe I should drop the use of Base class from declarative_base?
  2. Where should I define validators - in domain objects or in repository objects?
  3. Should SimpleService(session) take a session in the initializer or specific repositories that I will create in the main function? Now I create it here:
class SimpleService:
    """Creates simple service."""

    def __init__(self, session):
        self.session = session
        self.post_repository = PostRepository(session)
        self.comment_repository = CommentRepository(session)
  1. What should I change or improve in whole project? Please give me a quick review.

Short summary of project

Project structure:

.
│   main.py
│   README.rst
│   requirements.txt
│
├───blog
│   │   __init__.py
│   │
│   ├───domain
│   │       comment.py
│   │       post.py
│   │       __init__.py
│   │
│   ├───repository
│   │   │   __init__.py
│   │   │
│   │   ├───crud
│   │   │       base.py
│   │   │       comment.py
│   │   │       post.py
│   │   │       __init__.py
│   │   │
│   │   └───models
│   │           base.py
│   │           comment.py
│   │           post.py
│   │           __init__.py
│   │
│   └───service
│           simple.py
│           __init__.py

Business model in domain/post.py:

from dataclasses import dataclass
# from .comment import Comment

@dataclass
class Post:
    id: str
    title: str
    content: str
    # Should I implement it? If yes how to implement it here and in crud?
    # comments: list[Comment]
    #
    # def __post_init__(self):
    #     if self.comments is None:
    #         self.comments = []

Orm model in repository/models/post.py:

from sqlalchemy import Column, String
from sqlalchemy.orm import relationship
from .base import Base

class Post(Base):
    __tablename__ = 'posts'

    id = Column(String, primary_key=True)
    title = Column(String)
    content = Column(String)
    executions = relationship('Comment', backref='post')

    def __repr__(self):
        return f'Post(id={self.id}, title={self.title}, content={self.content})'

CRUD class to manage orm model repository/crud/post.py:

from blog.repository.crud.base import BaseRepository
from blog.repository.models.post import Post as PostModel
from blog.domain.post import Post as PostEntity


class PostRepository(BaseRepository):
    """Creates object to manage post in data in database."""
    def __init__(self, session):
        self.session = session

    def create(self, post):
        """Creates post."""
        post_model = PostModel(id=post.id, title=post.title, content=post.content)
        self.session.add(post_model)
        self.session.commit()

    def get_by_id(self, id_):
        """Gets post by id."""
        post_model = self.session.query(PostModel).get(id_)
        if post_model is not None:
            return PostEntity(id=post_model.id, title=post_model.title, content=post_model.content)

Main business logic in service/simple.py:

import time
import uuid
from blog.repository.crud.comment import CommentRepository
from blog.repository.crud.post import PostRepository
from blog.domain.post import Post
from blog.domain.comment import Comment


class SimpleService:
    """Creates simple service."""

    def __init__(self, session):
        self.session = session
        self.post_repository = PostRepository(session)
        self.comment_repository = CommentRepository(session)

    def run(self):
        """Performs fake operation (main app logic)."""
        print('Fake action in simple service...')

        print('Creating posts...')
        time.sleep(1)
        post_id = str(uuid.uuid4())
        post = Post(id=post_id,
                    title='Post 1',
                    content='Lorem Ipsum is simply dummy text of the printing and typesetting industry.')
        self.post_repository.create(post)

        print('Creating comment...')
        time.sleep(1)
        comment_id = str(uuid.uuid4())
        comment_1 = Comment(id=comment_id,
                            content='Lorem Ipsum has been the industry\'s standard dummy text',
                            post=post)
        self.comment_repository.create(comment_1)

        print('Getting post from db...')
        time.sleep(1)
        print(self.post_repository.get_by_id(post_id))

        print('Getting comment from db...')
        time.sleep(1)
        print(self.comment_repository.get_by_id(comment_id))

Entrypoint of app in main.py:

import os
from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from blog.repository.models.base import Base
from blog.service.simple import SimpleService


def main():
    """Entrypoint of app."""

    database_uri = os.getenv('DATABASE_URI')  # set DATABASE_URI=sqlite:///data.db
    if database_uri is None:
        database_uri = 'sqlite:///:memory:'
    engine = create_engine(database_uri)

    Base.metadata.create_all(engine)

    with Session(engine) as session:
        simple_service = SimpleService(session)
        simple_service.run()


if __name__ == '__main__':
    main()
\$\endgroup\$
5
  • \$\begingroup\$ You should explain what your code is about. Hope I understood it right. \$\endgroup\$
    – convert
    Commented Feb 22, 2023 at 21:54
  • 1
    \$\begingroup\$ Hi, could you please edit your question to directly include all the code you want reviewed, rather than just linking to it? As a rule, we don't review code that's behind a link, since things can get confusing if the linked code is deleted or changes drastically in some other way \$\endgroup\$
    – Sara J
    Commented Feb 22, 2023 at 22:09
  • \$\begingroup\$ Edited as requested :) \$\endgroup\$
    – Minipami
    Commented Feb 22, 2023 at 22:36
  • 1
    \$\begingroup\$ Welcome to Code Review! Please edit your question so that the title describes the purpose of the code, rather than its mechanism. We really need to understand the motivational context to give good reviews. It's best to describe what value this code provides to its user. \$\endgroup\$ Commented Feb 23, 2023 at 9:07
  • \$\begingroup\$ Little nitpick: database_uri = os.getenv('DATABASE_URI', 'sqlite:///:memory:') \$\endgroup\$ Commented Mar 25, 2023 at 16:34

2 Answers 2

3
\$\begingroup\$

To expand a bit more on the other answer:

Does it make sense to use an ORM for a DDD project?

Yes, it's pretty common to use an ORM in a DDD project (pretty common = I am sharing my personal experience based on the projects that I have seen/worked on ). The key (IMO) is to ensure that the ORM does not leak into the domain layer. The repository layer (usually) acts as an interface between the domain layer and the persistence layer, so the domain layer should not be aware of the ORM implementation details. One way to achieve this is to define an abstraction layer on top of the ORM implementation, which is used by the repository layer. In your implementation, the ORM implementation details are in the repository/models directory, which is separated from the domain layer.

Regarding the use of the Base class from declarative_base, it depends on your project's requirements. If you don't need the functionality provided by the Base class, you can drop it.

Where should I define validators - in domain objects or in repository objects?

Validators should be defined in the domain objects because they are responsible for enforcing business rules. The repository layer's responsibility is to persist and retrieve domain objects, so it should not be concerned with enforcing business rules.

Should SimpleService(session) take a session in the initializer or specific repositories that I will create in the main function?

It's better to pass specific repositories to the SimpleService class's initializer because it helps to decouple the service layer from the persistence layer. In your implementation, you are passing the session to the SimpleService class's initializer and creating repositories inside the initializer. It's better to create the repositories in the main function and pass them to the SimpleService class's initializer.


Regarding the overall implementation, it's a good start, and you have separated the domain layer from the persistence layer. Here are some suggestions for improvements:

  • Define interfaces for the repository layer and have concrete repository implementations that implement these interfaces. This way, you can swap out repository implementations without affecting the service layer or the domain layer.
  • Consider using a dependency injection framework to manage dependencies between layers.
  • Use a logger to log events and errors in your application.
  • Implement exception handling and error reporting to help with debugging and error recovery.
  • Consider implementing more functionality in your domain objects, such as validation and behaviour, instead of relying on data classes.
\$\endgroup\$
1
\$\begingroup\$

The way I understand DDD, it is mostly about your Domain models being created by repositories and then "living" their life, doing their domain logic, that gives your business some value. There is usually very little persistence and if you do persistence, you call your repositories, but the core is outside of repositories and in between and while interacting between your models. You do actions that have meaning for you.

Imho CRUD doesn't align with that very much, because that is mostly about about repositories and there is almost 0 real DDD style code. CRUD is just bad usecase for DDD. Basically you create your models, load them. Then I expect the real DDD to begin and then nothing happens at all.

To answer your questions:

  1. I don't see definition of your Base class. Overall I don't see problem with using ORM with DDD, but it's good practice to have your models completely unaware of any kind of ORM (that doesn't mean ORM can't be applied to them, it's just more orthogonal). Cleaner and safer approach is to separate ORM and your domain models, but that adds a lot of boilerplate and duplication so it's a tradeoff. Definitely depends on your usecase.

  2. The most important for you is to have valid domain objects, that is the bread and butter of your code, so I suggest putting them in their constructor. That way you ensure they are always valid (I mean we are in python, but you an assume).

  3. The cleaner approach is to pass repositories. Repositories are very common and well known abstraction in DDD. You can then easily replace your persistence layer by replacing repositories. Session is something more fuzzy and "implementation" detail from this perspective.

\$\endgroup\$
1
  • \$\begingroup\$ Hi, thanks for the answer, it was useful \$\endgroup\$
    – Minipami
    Commented Feb 26, 2023 at 9:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.