What is the Spring Modulith Moments component and which time-passage events does it publish?
answer
- spring-modulith-moments artifact
- HourHasPassed / DayHasPassed / ... / YearHasPassed
- @EventListener instead of @Scheduled
- granularity HOURS vs DAYS
- TimeMachine fast-forwards in tests
basics
~10 sMoments is a Spring Modulith helper (spring-modulith-moments) that publishes application events as time passes: HourHasPassed, DayHasPassed, WeekHasPassed, MonthHasPassed, QuarterHasPassed and YearHasPassed. Modules subscribe with @EventListener to run scheduled-style logic.
solid answer
~30 sMoments (artifact spring-modulith-moments) turns the passage of wall-clock time into ordinary Spring ApplicationEvents so modules can react without wiring their own scheduler. When a time boundary is crossed it publishes HourHasPassed, DayHasPassed, WeekHasPassed, MonthHasPassed, QuarterHasPassed and YearHasPassed. Each event carries the relevant instant — e.g. HourHasPassed.getTime() (a LocalDateTime) and DayHasPassed.getDate() (a LocalDate). You just add @EventListener/@ApplicationModuleListener methods. Behaviour is tuned via properties: spring.modulith.moments.granularity (HOURS or DAYS), zone-id, locale and quarter-start-month. The big win is testability: instead of waiting real hours, tests inject a TimeMachine and fast-forward time, so 'once a day' logic becomes deterministic and instant.
code
java · 22 lines@Component
class DraftExpiry {
// Reacts to a day boundary crossing instead of owning a cron job
@ApplicationModuleListener
void on(DayHasPassed event) {
LocalDate day = event.getDate();
expireDraftsOlderThan(day.minusDays(30));
}
// Finer-grained reaction; only fires when granularity = HOURS
@ApplicationModuleListener
void on(HourHasPassed event) {
LocalDateTime hour = event.getTime();
purgeStaleSessions(hour);
}
}
// application.properties
// spring.modulith.moments.granularity=HOURS
// spring.modulith.moments.zone-id=Europe/Berlin
// spring.modulith.moments.enable-time-machine=true # test profile onlygo deeper
Know it publishes time-passage events (DayHasPassed etc.) that you handle with @EventListener instead of writing a scheduler.
Know the full event set, the granularity/zone-id properties, and that HourHasPassed is suppressed under DAYS granularity.
Articulate the decoupling benefit (modules react to time without owning schedulers) and the testability angle via TimeMachine.
Weigh Moments vs Quartz/ShedLock for distributed, cron-precise, or missed-fire-sensitive workloads and set zone/granularity policy.
## What Moments is Spring Modulith ships an optional module, **spring-modulith-moments**, that models the *passage of time* as first-class Spring application events. Instead of each module declaring its own `@Scheduled` cron job to do 'something every day', a module can simply listen for a `DayHasPassed` event. This keeps scheduling concerns decoupled and — crucially — makes time-driven behaviour **testable without waiting for real time to elapse**. ## The events When the passage of time crosses a boundary, the `Moments` bean publishes one of these events (each is a plain immutable value object, published via Spring's `ApplicationEventPublisher`): - `HourHasPassed` — `getTime()` returns a `LocalDateTime` for the hour that just ended. - `DayHasPassed` — `getDate()` returns a `LocalDate`. - `WeekHasPassed` — carries the year and ISO week. - `MonthHasPassed` — carries year and `Month`. - `QuarterHasPassed` — carries year and quarter (quarter start month is configurable). - `YearHasPassed` — carries the `Year`. Boundaries cascade: crossing midnight publishes `HourHasPassed` **and** `DayHasPassed`; the last day of a month publishes the day, week (if applicable), and month events. ## How you consume them Any Spring bean in any module: ```java @Component class DigestSender { @ApplicationModuleListener void on(DayHasPassed event) { sendDailyDigest(event.getDate()); } } ``` `@ApplicationModuleListener` is Modulith's meta-annotation combining `@Async` + `@Transactional` + `@TransactionalEventListener(phase = AFTER_COMMIT)`; a plain `@EventListener` also works. ## Configuration properties - `spring.modulith.moments.granularity` — `HOURS` (default) or `DAYS`. With `DAYS`, `HourHasPassed` is never emitted. - `spring.modulith.moments.zone-id` — the zone used to decide boundaries (e.g. `Europe/Berlin`). - `spring.modulith.moments.locale` — affects week calculation (first day of week). - `spring.modulith.moments.quarter-start-month` — lets a fiscal quarter start in a month other than January. ## Time source Moments reads the current time from a `Clock`. In production it advances with the real clock; the boundary-crossing detection needs the app to observe the passing time. In **tests** you replace this driving mechanism with a `TimeMachine` (enabled by `spring.modulith.moments.enable-time-machine=true`) and call `shiftBy(Duration)` to fast-forward, which fires all crossed events synchronously. ## When to use it Good for coarse-grained, in-process 'react as time passes' logic that belongs to a module (daily digests, expiring drafts hourly, monthly rollups). It is **not** a replacement for a full scheduler like Quartz when you need precise cron expressions, missed-fire recovery, clustering coordination, or sub-hour precision. ## Gotchas - Add the `spring-modulith-moments` starter/dependency — it is not pulled in by core Modulith. - With default `HOURS` granularity a busy app emits hourly events; make listeners cheap/idempotent. - Events reflect boundaries in the **configured zone**, so daylight-saving changes and zone choice matter.
- Which event(s) fire at midnight with default granularity?Both HourHasPassed (for the 23:00 hour ending) and DayHasPassed fire, because crossing a day boundary also crosses an hour boundary — boundaries cascade from finest to coarsest.
- Does Moments replace a scheduler like Quartz?No. It's for coarse, in-process, module-local reactions to elapsed time. It has no cron precision, no clustered single-firing, and no missed-fire recovery; use a real scheduler for those needs.
saying these in an interview costs you the question
- Claiming Moments gives cron-like precision or sub-hour scheduling
- Thinking HourHasPassed always fires regardless of granularity
- Believing Moments is included in core Spring Modulith without the extra dependency
- Confusing Moments (time events) with the ApplicationEventPublisher for domain events only