What is the common approach to reconstruct a domain model e.g. from a database model?

Given a domain model for a simplified todo app

type Todo struct {
    title          string
    isMarkedAsDone bool
    modifiedAt     time.Time
}

func NewTodo(title string) (*Todo, error) {
    if title == "" {
        return nil, errors.New("title is empty")
    }

    todo := &Todo{
        title:          title,
        isMarkedAsDone: false,
        modifiedAt:     time.Now(),
    }

    return todo, nil
}

func (t *Todo) GetTitle() string {
    return t.title
}

func (t *Todo) IsMarkedAsDone() bool {
    return t.isMarkedAsDone
}

// other getters...

func (t *Todo) Rename(newTitle string) error {
    if t.isMarkedAsDone {
        return errors.New("todo is already marked as done")
    }

    if newTitle == "" {
        return errors.New("new title is empty")
    }

    t.title = newTitle
    t.modifiedAt = time.Now()

    return nil
}

func (t *Todo) MarkAsDone() error {
    if t.isMarkedAsDone {
        return errors.New("todo is already marked as done")
    }

    t.isMarkedAsDone = true
    t.modifiedAt = time.Now()

    return nil
}

// other setters...

Saving this todo to a store is no problem since I can access the fields via getters. But when I query the store for todos that are marked as done I can’t reconstruct a domain object from the returned data.

The constructor function takes no isMarkedAsDone = true parameter ( + isMarkedAsDone ) and if I would try to create a new domain todo and call the MarkAsDone function on it I would overwrite the field modifiedAt which is wrong then.

What is a common approach to solve this?

  • Make everything public? ( feels wrong to me, consumers could put the domain object in invalid state )
  • Change the whole constructor function to accept all fields and validate all of them, so consumers have to provide all fields from outside?
  • Keep everything as is but provide a reconstruct function in the same package accepting all fields ( + validation ) that creates a domain model and has write access to the private fields since it lives in the same package?

The idiom for this kinf of problem in Go is use optional Pattern. Please check

And in some cases i aldo apply Builder pattern

Thanks for your reply. But how would you handle the modifiedAt field? Having a WithModifiedAt-function?

Normally you would never have such a function because this field will be set on every modification. So if you go for the options pattern you could also setup one big function that validates every parameter, no?

Yes, it does not need a getter/setter and create an option function per field

True, but when calling a function like Rename or MarkAsDone on Todo you would still update the modifiedAt field, right?

What if consumers then call WithModifiedAt again on it? I personally think they shouldn’t be able to do so …

This is the best option imo.

Making all fields public violates encapsulation and accepting all fields in constructor can lead to misusing it.

Hi Joachim,

I noticed this question is a bit older, but I wanted to offer some feedback on the code. Please take this as a friendly suggestion to make your code more approachable.

It seems like many people code in this style, and I’m curious why that is. :thinking: Perhaps it’s seen as a clever way to code, but sometimes simplicity can be more effective and easier to understand for everyone.

I want to provide some advice that might help others who come across this thread as well.

Instead of focusing on a common approach, why not aim for a good approach? A domain model might not be necessary, and you don’t need constructors in this case. You can just initialize the app struct.

Here’s my suggestion:

Instead of using functions directly on your Todo, consider using the functions on your database connector. Use your db.Models directly instead of redefining them. (Assuming you are using SQL to store todos persistently.)?

Here’s some pseudocode to illustrate:

type App struct {
    Queries *db.Queries
}

func (app *App) getTodo(todoId uuid.UUID) (todo db.Todo, err error) {
    todo, err := app.Queries.GetTodo(context.TODO(), db.GetTodoParams{ID: todoId})
    // error handling
    return todo, err
}

func (app *App) createTodo(name string) (err error) {
    _, err := app.Queries.CreateTodo(context.TODO(), db.CreateTodoParams{Name: name})
    // error handling
    return err 
}

Try to avoid forced OOP. Since you’re working on todo lists, it’s a great opportunity to practice. By implementing both approaches, you’ll be able to see which one works better. Often, the simpler solution proves to be more effective, and real-life tasks will introduce enough complexity on their own.

Go is simple.

I hope this helps. Have a wonderful weekend! :sun_with_face:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.