Q: Should we fetch the Engine and Model from the database by their ids before building the domain model ?
Yes, this's what repositories do. If
Car is an aggregate, the repository should be capable of fetching whatever
Car needs for its initialization. It might load
Model directly from the data store or ask for them to other components. You also could use services to retrieve
Model and send'em to the repository alongside with the attribute
Q: Also in the case when in our Domain Model we have some complex properties that are not always set, is it enough to just pass null as a parameter, or they shouldn't appear at all in the constructor, and give them a Setter ?
If any of the attributes are optional, it's important to make this as clear and explicit as possible. This said, it's preferable to take them off the constructor and define setters (or methods with the due parametrization) because it simplifies the code and improves readability.
To initialise entities in different states, you could overload constructors or create static factory methods.2
If you are concerned about the access to the setters you have different ways to control it. The first that comes to mind is through access modifiers. However, you could rely on well-known OOP design practices (inversion of control or interface segregation to mention any) or design patterns.1
S: Using Ids in Domain Models is not a good practice
That's fairly true, overall when we turn the persistence model into the domain one. In such cases, the implementation details of the persistence bypass the boundaries of the persistence and become part of the implementation detail of the domain too. Raw strings and numbers become identifiers when, from the DDD standpoint, they are most likely the worst representation for an identifier. In DDD, identifying entities and aggregates might take something else than a simple string.
Since @Casablanca has mentioned Vaughn Vernon, I will share another link where he speaks about correlating boundaries (aggregates) through identifiers.
1: Defensive programming as security is a matter of costs vs benefits. Don't go crazy making everything private (narrowing down scopes to the absurd) at expenses of simplicity and readability. Unless you have a good reason for it.
2: Some languages implement the notion of optional parameter. As soon the intention is clear (what's optional and what's not), these parameters should work too.