0

I have an app with "clean architecture" classic layers:

  • Ports (such as HTTP, GRPC, GRAPHQL resolvers)

  • Services (CRUD methods and others)

  • Adapters

  • Domain

My app has of course multiple "models" such as:

  • Player

    • id: String (mandatory)
    • firstname: String (mandatory)
    • lastname: String (mandatory)
    • age: Number (NOT mandatory)
    • motto: String (NOT mandatory)
    • team_id: String (mandatory)
  • Team

    • id: String (mandatory)
    • name: String (mandatory)
  • Coach

    • id: String (mandatory)
    • firstname: String (mandatory)
    • lastname: String (mandatory)

The typical flow is:

  • DTO structs (Data Transfer Objects) such as PlayerCreateInput and TeamUpdateInput are created in Ports layer's resolvers (from user requests);

  • Service layer methods are called (such as Create, Update, Delete and so on) with that DTO as input;

  • Domain logic is called (if needed);

  • Adapters methods for persistence or other needs are called;

  • Ports methods return the result.

Everything is working good and I'm pretty happy!

Today the need has arisen to be able to save each model as a draft: not complete with all fields even if mandatory.

The first idea I had was to add a "status" field to each model like:

  • state: enum (can be "DRAFT" or "CONFIRMED" for example)

As you can imagine I have a frontend in my app with generated types for each model. These types obviously respect the mandatory conditions of the fields so as to respect the principle "make invalid state unrepresentable".

In fact for each model I have a function called "new(args)" which checks all args before returning.

If I were to allow saving a model without mandatory fields I could no longer use this logic and would have to create other types such as "PartialDomainPlayer" which has all fields optional.

Then based on application logic I should convert the PartialDomainPlayer into DomainPlayer where needed handling the opportune error.

But the same also applies to databases: today the mandatory fields have a NOT NULL which in this case should be removed because the row in the DB can be saved with NULL values.

Another way to save drafts of each model would be to save those drafts in another database table, such as "drafts" :).

But I can't understand how and if it is possible to correctly paginate them, for example in the "players list" page, on the first page, with a limit of 5 players, ordered by "created_at DESC", if there are others players in the "drafts" table I would have to merge two queries with the same criteria on the backend, but in any case I would also need to change the type on the frontend from Player to PartialPlayer with a consequent increase of the code like: if (player.firstname) { use firstname here }. Now instead I can trust that Player's firstname is not null.

I'm wondering what the best practices are for this type of IT problem.

What do you think?

3
  • You’re asking multiple questions, can you rewrite your post to focus on a single problem? Make sure it’s not a code question and not asking for best practices.
    – Rik D
    Commented Dec 16, 2023 at 20:27
  • The question is one and one only. The best practices is because I would like to know your opinions.
    – Fred Hors
    Commented Dec 16, 2023 at 21:46
  • I think it's great that you're aware of these difficulties because it helps you clarify your requirements and priorities. You can relax a Player to be partially "invalid", but then all of the code using Player objects needs to be aware of that. Or you can model this as a separate entity, but then of course those pieces of code get more complicated that do want to treat them as the same. None of these solutions are better, they're just different designs with different tradeoffs. There is no way to magically make that complexity go away.
    – amon
    Commented Dec 17, 2023 at 21:56

1 Answer 1

2

The trick here is to change your perspective about what "invalid state" means. A player in the "draft" state is perfectly valid with no name. This is invalid for a (active, published, official, current, complete, submitted... whatever your non-draft status is) player. From a domain model perspective, the transition from draft to active (or whatever you call it) is where you enforce those additional class invariants.

At a philosophical level, this frees your design to allow:

  • Nullable columns in the database.
  • A single table to store "draft" and "active" data.
  • Missing and non-sensical values in the domain model.
  • Behavior in the domain model that enforces additional rules when transitioning away from the "draft" state.

The status of an entity determines what "valid" means. We get used to database constraints enforcing the simpler business rules, like "every player must have a name." This approach pushes even more logic into your domain layer.

You might need to add defensive programming to guard against these missing values in case a defect slips by, or some other system inserts data incorrectly, but that's the cost of allowing users the convenience of partially creating something. You will need to weigh the benefits of allowing partial data with the drawbacks of peppering your code with additional checks and error handling.

Another option is to use a document database or NoSQL solution to simply store the user input values, but as you noted you would need to combine two queries into one. Instead of combining queries, consider making them separate use cases: active players and draft players, for example. Beware the tight coupling introduced by storing structured used input in a database. Changes to the user interface data structure would still need to work with old drafts stored months, of not years ago. You can help mitigate this by discarding drafts after a certain amount of time. Just be sure to warn users about this while filling information out and somewhere that lists the drafts they created. Again, you introduce more complexity, so the draft status should be worth the effort.

Unfortunately there are no easy answers here. You can't allow the user to violate your current rules and force them to adhere to the rules at the same time. The rules need to change, and you'll need to analyze the downstream impacts of allowing partial information. This is more than "just a status" so adjust your estimate accordingly.

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.