Let me introduce you into a minimalistic and made-up case with real-case chronological order, updates and problems.
User with ID only
I had a system, and it had User
entity with ID
column only. So, there was a class User
and SQL table Users
:
[Table("Users")] // database table
public class User
{
[Key]
[Column("Id")] // database column
public long Id { get; set; }
}
New properties
One day, a product owner comes to me and asks me to introduce 4 new properties:
- birthday
- age maturity (18 y.o. and above)
- subscribtion end (paid until) date
- is active, which means that subscription has not expired yet
Of course, at first I propose to not store “age maturity” and “is active”, since it is a secondary evaluated value.
However, business analyst tells me: OK, do it your way, but we will need to see export table of all users with their maturity and activeness statuses, with up to 10000 users.
Now, I see 3 solutions and cannot decide which is better in terms of summarized assessment of architecture, performance and future supportability.
Possible solutions
1. Evaluable properties in domain model
[Table("Users")] // database table
public class User
{
[Key]
[Column("Id")]
public long Id { get; set; }
[Column("Birthday")]
public DateTime Birthday { get; set; }
public bool IsMature
{
get { return (DateTime.Now - Birthday).Years > SystemCfg.MaturityAge; }
}
[Column("PaidUntil")]
public DateTime? PaidUntil { get; set; }
public bool IsActive
{
get { return PaidUntil.HasValue && PaidUntil.Value >= DateTime.Now; }
}
}
Pros:
- Actual values: values are always actual
- Business-logic placement: business-logic is stored in a single place
Cons:
- Performance: in real example,
PaidUntil
is actually a property which takes some time to be calculated, sometimes with web-requests, so calculating it for 10000 users dynamically can become dramatically slow in future
2. Database-stored properties, recalculated at sign in
[Table("Users")] // database table
public class User
{
[Key]
[Column("Id")]
public long Id { get; set; }
[Column("Birthday")]
public DateTime Birthday { get; set; }
[Column("IsMature")]
public bool IsMature
[Column("PaidUntil")]
public DateTime? PaidUntil { get; set; }
[Column("IsActive")]
public bool IsActive { get; set; }
}
// on sign in:
user.IsMature = (DateTime.Now - user.Birthday).Years > SystemCfg.MaturityAge;
user.IsActive = PaidUntil.HasValue && PaidUntil.Value >= DateTime.Now;
_userRepository.Update(user);
if (!user.IsActive)
return AuthResult.UserIsInactive;
Pros:
- Performance: values are only recalculated on sign in, which brings the best performance among these options
Cons:
- Business-logic placement: business-logic is stored in a single place, but model is now anemic
- Actual values: values are mostly not actual for report / export views, since are updated on user sign in
3. Database-stored properties, recalculated on schedule
Same model, but data is updated by a special Windows Service, which runs through my database and keeps data up to date.
Pros:
- Performance: values are calculated on a separate thread / application / server
- Actual values: values are guaranteed to be actual UNTIL update server becomes overloaded or down
Cons:
- Business-logic placement: business-logic is stored in a separate, non-domain application
- This approach sounds weirds
So, what approach should I use for this special case?
What approaches do you and people usually use?
5
Your first solution : calculate the domain model is the best.
In fact though you have two types of calculated fields here. Ones which change depending on the date on which you look at the object and ones which change depending on an system event (paidUntilDate)
Fields which change according to when you look at them have to be calculated on the fly or risk being out of date. The export will be incorrect the day after its generated and you should probably attempt to avoid such fields at all where possible.
However the PaidUntilDate should be calculated when the event which changes it happens. Presumably when the customer buys a subscription.
This gets around your long calculation problem. Perhaps you can just do a join to a subscriptions table instead of the calc.
0