Current date is an infrastructure concern
Getting the current date is an easy task in most languages. In Java you can call LocalDate.now()
, in C# DateTime.Now
or Rust chrono::offset::Local::now()
. This seems quite easy to do, so most developers will do it close to where the date is needed. The argument this article presents is that you want to push this code into the infrastructure layer of your code. This applies both to getting the date and time, but for simplicify, the date is the focus.
The following are typical problems that you get when using the now functions directly in the code:
- In most applications the current date should be the same across any action taken. For example in a web request the same current date should be used even if executes over 5 seconds the the date "technically" changes.
- Time travel becomes a more involved process where testing effects over time is hard both in functional and automatic tests.
- Tests lose idempotent when the date is a factor.
- Tests suddenly fail when the year switches (or the common: tests always fails on 29. February)
An intuitive argument for why this should be handled in the "infrastructure" part of the code is that the underlying logic will make a system call to get the current date from the OS running the program. System calls are the essence of an infrastructure concern as it relies directly on the infrastructure the application is running on.
In practice the code should be split between the layers:
The CurrentDateProvider should be defined as a interface/contract/trait in the application layer and defined in the infrastructure layer.
Infrastructure layer should call the relevant function for getting the current date in the relevant library.
Application layer should forward the current date:
fn application_logic(current_date_provider: &impl CurrentDateProvider) -> Result {
// some application logic
let should_do_extra_thing = domain_logic(current_date_provider.get());
if should_do_extra_thing {
// do extra thing
}
// some other application logic
}
(Integration tests in the application layer are usually more involved, so they are not included here)
Domain layer should use the current date (important to remember tests):
fn domain_logic(current_date: DateTime<Utc>) -> bool {
// Special case for some business event
return current_date.year == 2024 && current_date.month == 10 && current_date.day == 4;
}
#[test]
fn true_in_2024_10_4() {
let current_date = Utc.with_ymd_and_hms(2024, 10, 4, 0, 1, 1).unwrap();
assert!(domain_logic(current_date))
}
fn false_in_2025_10_4() {
let current_date = Utc.with_ymd_and_hms(2025, 10, 4, 0, 1, 1).unwrap();
assert!(!domain_logic(current_date))
}