Abstracting Java System Time Creation with InstantSource to Improve Testability

When obtaining system time (current time) in Java, it's common to use methods like Instant.now() or LocalDateTime.now(). However, these methods depend on the OS system clock, making it difficult to control system time during testing.

The JDK provides classes for abstracting system time creation:

  • java.time.Clock - Added in JDK 8
  • java.time.InstantSource - Added in JDK 17

The difference between Clock and InstantSource is that the former holds timezone information. InstantSource only handles java.time.Instant generation. Also, Clock is an abstract class, while InstantSource is an interface.

When using Clock, you obtain date and time as follows:

Clock clock = Clock.systemUTC();
Clock clock = Clock.systemDefaultZone();

Instant now = clock.instant();
OffsetDateTime now = OffsetDateTime.now(clock);
LocalDateTime now = LocalDateTime.now(clock);
LocalDate now = LocalDate.now(clock);

When using InstantSource, you obtain time as follows:

InstantSource instantSource = InstantSource.system();

Instant now = instantSource.instant();

System time (Instant) and user localization (ZoneId) are inherently separate concerns, but Clock couples them together. InstantSource was added in JDK 17 based on the idea that there should be a simple interface for obtaining only system time. More detailed background can be found here.

The following examples use InstantSource, but since Clock implements the InstantSource interface, Clock instances can also be used as InstantSource. For systems used only within Japan where the timezone is fixed, using Clock might be more convenient for generating LocalDate and similar operations. Note that conversion from InstantSource to Clock can be done as follows:

Clock clock = instantSource.withZone(ZoneId.systemDefault());

In actual applications, InstantSource is commonly injected using Dependency Injection (DI) containers. For example, when using Spring Boot, you would define a Bean as follows:

import java.time.InstantSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
class AppConfig {

	@Bean
	InstantSource instantSource() {
		return InstantSource.system();
	}

}

Since InstantSource is a Functional Interface, it can also be implemented with lambda expressions:

	@Bean
	InstantSource instantSource() {
		return Instant::now;
	}

When you want to create an Instant in your code, inject and use InstantSource:

@Service
public class MessageService {
	private final InstantSource instantSource;

	public MessageService(InstantSource instantSource) {
		this.instantSource = instantSource;
	}

	public Message createMessage(String content) {
		Instant now = instantSource.instant();
		return new Message(content, now);
	}
}

Since InstantSource is an interface, it's easy to replace during testing. Here's an example test code. This uses Mockito to mock InstantSource and return a specific time:

import java.time.Instant;
import java.time.InstantSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@ExtendWith(SpringExtension.class)
@Import(MessageService.class)
class MessageServiceTest {

	@Autowired
	MessageService messageService;

	@MockitoBean
	InstantSource instantSource;

	@Test
	void createMessage() {
		given(instantSource.instant()).willReturn(Instant.parse("2026-01-01T00:00:00.00Z"));
		Message message = messageService.createMessage("Hello, World!");
		assertThat(message.toString()).isEqualTo("Message[content=Hello, World!, timestamp=2026-01-01T00:00:00Z]");
	}

}

When you want to create a LocalDate, do it as follows:

		ZoneId zoneId = ZoneId.systemDefault(); // or ZoneId.of("Asia/Tokyo");
		LocalDate now = instantSource.instant().atZone(zoneId).toLocalDate();

Alternatively, it might be better to set the timezone at injection time:

@Service
public class MessageService {
	private final Clock clock;

	public MessageService(InstantSource instantSource) {
		this.clock = instantSource.withZone(ZoneId.systemDefault());
	}

	public Message createMessage(String content) {
		LocalDate now = LocalDate.now(this.clock);
		return new Message(content, now);
	}
}

When you want to change ZoneId per user, you can use methods like org.springframework.format.datetime.standard.DateTimeContextHolder to hold ZoneId in thread-local storage.

A common pattern in enterprise development is obtaining system time from a database. This is effective when you want to test specific times during system testing. Here's an example of obtaining system time from a database, assuming PostgreSQL:

Suppose you have a table for setting specific system times:

CREATE TABLE IF NOT EXISTS system_date
(
    date_time TIMESTAMP WITH TIME ZONE
);

Here's an example that uses the datetime from this table if data exists, or the database's current time as system time if no data exists:

import java.time.InstantSource;
import java.time.OffsetDateTime;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.simple.JdbcClient;

@Configuration(proxyBeanMethods = false)
class AppConfig {

	@Bean
	InstantSource jdbcInstantSource(JdbcClient jdbcClient) {
		return () -> jdbcClient.sql("SELECT COALESCE((SELECT date_time FROM system_date), NOW())")
			.query(OffsetDateTime.class)
			.single()
			.toInstant();
	}

}

Alternatively, if you have a table that holds an offset (in minutes) from the current time in the database:

CREATE TABLE system_date
(
    offset_minutes INT NOT NULL
);

Here's an example that adds the value in minutes to the system time if data exists in this table, or adds 0 if no data exists:

import java.time.InstantSource;
import java.time.OffsetDateTime;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.simple.JdbcClient;

@Configuration(proxyBeanMethods = false)
class AppConfig {

	@Bean
	InstantSource jdbcInstantSource(JdbcClient jdbcClient) {
		return () -> jdbcClient.sql("""
				SELECT
				    NOW() + MAKE_INTERVAL(
				        mins => COALESCE(MAX(offset_minutes), 0)
				    )
				FROM
				    system_date
				""").query(OffsetDateTime.class).single().toInstant();
	}

}

For example, if you want to test with a time one hour ahead, you can insert a record like this:

INSERT INTO system_date(offset_minutes) VALUES (60);

When testing is complete and you delete this record, it will return to the normal current time.

Using these approaches, you can change the system time by simply modifying the database contents without restarting the application.


By using InstantSource, you can abstract system time acquisition and freely control time during testing. If you're directly using Instant.now() or LocalDate.now(), I recommend changing to go through InstantSource to write more testable code.