Spring Modulith — Phase 0–8 incremental groundwork#28
Draft
alfredorueda wants to merge 26 commits into
Draft
Conversation
Add Gherkin behavioural specs for watchlists and Level 1 Market Sentinel detection. Made-with: Cursor
Introduce Watchlist, AlertEntry, WatchlistId and invariants for Level 1 price-threshold alerts. Made-with: Cursor
Add ports and services for watchlist commands and Market Sentinel CQRS read-side detection, with unit tests and contract test scaffolding. Made-with: Cursor
Implement WatchlistPort and WatchlistQueryPort for both JPA and MongoDB, including scalable read-side projection queries and contract tests. Made-with: Cursor
Add inbound Telegram command adapter (webhook + setMyCommands), outbound Telegram notification adapter, REST endpoints for watchlists, and bootstrap wiring/scheduler configuration. Made-with: Cursor
Use Java 21 record patterns in Telegram command handler, refactor contract tests to include assertions, and clean up Mongo query DTOs/literals and assertThrows lambdas. Made-with: Cursor
…Signal and related classes
…ntinel-telegram feat: watchlists + market sentinel + telegram bot
- Import spring-modulith-bom in root dependencyManagement. - Add spring-modulith-starter-core (compile) and spring-modulith-starter-test (test) to bootstrap. - Add spring-modulith-events-api to the notification adapter for @ApplicationModuleListener.
Watchlist no longer carries a Telegram-specific notification id. Notification routing is now the responsibility of a dedicated Notifications module that listens to the in-process WatchlistAlertTriggeredEvent. - Remove userNotificationId field, getter and constructor parameter. - Add static rehydrate(...) factory used by persistence mappers. - Update WatchlistTest accordingly and add reflection-based sanity check ensuring no notification-related members remain.
…tAlertTriggeredEvent Replace the direct call from MarketSentinelService to a NotificationPort with the publication of a pure domain event. The application Maven module remains Spring-free thanks to the new outbound DomainEventPublisher port. - Add cat.gencat.agaur.hexastock.watchlists.WatchlistAlertTriggeredEvent (pure record carrying business data only: watchlistId, userId, ticker, alertType, threshold, currentPrice, occurredOn, message). - Add port out DomainEventPublisher (Spring-free interface). - MarketSentinelService now depends on (WatchlistQueryPort, StockPriceProviderPort, DomainEventPublisher, Clock); for each triggered alert it builds and publishes the event. - Drop obsolete NotificationPort and BuySignal. - Drop chatId parameter from WatchlistUseCase.createWatchlist and WatchlistService. - TriggeredAlertView projection now exposes (watchlistId, ownerName, listName, ticker, thresholdPrice). - Update unit and contract tests accordingly.
…atchlist mappings - WatchlistJpaEntity / WatchlistDocument no longer persist the chat id. - Mappers use Watchlist.rehydrate(...) for reconstitution. - Query projections now expose watchlistId (no notification id).
…omain - REST: WatchlistDTOs.CreateWatchlistRequestDTO and WatchlistResponseDTO no longer carry userNotificationId; WatchlistRestController updated. - Telegram inbound: TelegramCommandHandler keeps using chatId only for the bot reply; createWatchlist is invoked with (ownerName, listName) only.
A new application module under cat.gencat.agaur.hexastock.notifications that
listens to WatchlistAlertTriggeredEvent via @ApplicationModuleListener and
dispatches the alert to channel-specific senders.
- NotificationChannel enum (LOG, TELEGRAM) and sealed
NotificationDestination hierarchy.
- NotificationRecipient and NotificationRecipientResolver port.
- InMemoryNotificationRecipientResolver: default resolver that always returns
a logging destination plus an optional Telegram destination read from
notifications.telegram.chat-ids.<userId>.
- NotificationSender interface + two implementations:
* LoggingNotificationSenderAdapter -> always-on default, structured SLF4J line.
* TelegramNotificationSenderAdapter -> @Profile("telegram-notifications"),
Telegram Bot API call via RestClient.
- WatchlistAlertNotificationListener: @ApplicationModuleListener (after-commit,
async, REQUIRES_NEW transaction) that resolves the recipient and routes to
the first sender that supports each destination's channel.
Removes the previous TelegramNotificationAdapter, NoopNotificationAdapter and
their test, replaced by the channel-agnostic listener + senders.
application - Add SpringDomainEventPublisher (config/events) as the Spring adapter for the application-side DomainEventPublisher port (delegates to Spring's ApplicationEventPublisher). - Update SpringAppConfig: MarketSentinelUseCase bean now takes a DomainEventPublisher instead of the deleted NotificationPort. - Annotate HexaStockApplication with @Modulithic(systemName = "HexaStock").
…tests - ModulithVerificationTest: scopes ApplicationModules to the new watchlists and notifications packages, runs verify(), confirms both modules are detected, and renders Modulith documentation under target/. - NotificationsEventFlowIntegrationTest: full @SpringBootTest that publishes a WatchlistAlertTriggeredEvent via the application port inside a transaction and asserts (Awaitility + Mockito spy) that LoggingNotificationSenderAdapter was invoked, exercising the complete publisher -> @ApplicationModuleListener -> sender flow.
Document the proof-of-concept that introduces an in-process Notifications module fed by the WatchlistAlertTriggeredEvent domain event. Covers motivation, architecture, module map, trade-offs (scoped Modulith verification, no externalisation, no live chat-id registration) and future extensions (persistent recipient store, Kafka externalisation, event-driven self-registration).
- Annotate notifications package with @ApplicationModule(allowedDependencies = {"watchlists"})
to express the only legal cross-module dependency on the consumer side.
- Add spring-modulith-api dependency to adapters-outbound-notification (transitively
pulls in only Modulith core types, no Spring context).
- Refine watchlists/package-info.java javadoc to document why @ApplicationModule is
intentionally NOT used there: the application Maven module must remain Spring-free
per ADR-007. Modulith still detects the module by package position.
- Module boundary intent is now explicit, source-controlled, and verifiable.
…ertions - Promote MODULE_PACKAGES to a single source of truth for which top-level packages are intentionally Modulith modules. New Bounded Contexts add an entry here (Phase 3+). - Replace ad-hoc 'anyMatch' assertion with containsExactlyInAnyOrder so that any unexpected promoted module fails the test. - Add notificationsOnlyDependsOnWatchlists: asserts the consumer's actual cross-module dependencies match the declared @ApplicationModule allow-list. - Add watchlistsHasNoOutgoingModuleDependencies: asserts the publisher remains a pure publisher with zero outgoing cross-module dependencies. - Document the staged verification strategy in the class javadoc, including why a global ApplicationModules.verify() is intentionally deferred to Phase 6 of the global refactoring plan.
Phase 3 of the Spring Modulith global refactoring is intentionally a no-code clarification step: the autonomy rules forbid large package moves without explicit review, and a Portfolio extraction would touch domain, application, JPA adapter, Mongo adapter, and REST adapter modules. This commit catalogues every legacy class that will eventually move to cat.gencat.agaur.hexastock.portfolios so the future Phase 3 PR can be performed mechanically and reviewed line by line. No production code changes.
Phase 4 of the Spring Modulith global refactoring is intentionally a no-code clarification step. Catalogues every legacy class that will eventually move to cat.gencat.agaur.hexastock.marketdata, lists current consumers (Portfolio and Watchlists/Market Sentinel), and documents the deferred rename of StockPriceProviderPort to MarketDataPort. No production code changes.
Replace InMemoryNotificationRecipientResolver, which hardcoded knowledge of the Telegram channel, with a CompositeNotificationRecipientResolver that aggregates profile-gated NotificationDestinationProvider beans: - LoggingNotificationDestinationProvider (always active, default channel). - TelegramNotificationDestinationProvider (only @Profile("telegram-notifications")). Boundary hardening achieved: - When the Telegram profile is OFF, no Telegram chat-id property is read and no TelegramNotificationDestination is ever produced; the listener no longer logs the spurious 'no sender available for channel TELEGRAM' warning that could occur when chat-ids were configured but the sender bean was absent. - Adding a new channel (email, SMS, ...) is now a zero-change task on the resolver: ship one DestinationProvider + one Sender, both gated by the same profile. - Watchlists module is unaffected and remains channel-agnostic. All 57 tests pass (up from 55; +2 from Phase 2 module verification expansion).
Adds CompositeNotificationRecipientResolverTest covering the post-Phase-5 contract: - empty provider list yields a recipient with zero destinations. - multiple providers are aggregated in order. - a provider returning an empty list contributes nothing. These pure-unit tests do not require Spring; they document the resolver's behaviour independently of any specific channel and pin the boundary established in Phase 5.
- Update SPRING-MODULITH-NOTIFICATIONS-POC.md references to the renamed CompositeNotificationRecipientResolver and document the new NotificationDestinationProvider per-channel SPI design. - Expand MODULITH-BOUNDED-CONTEXT-INVENTORY.md sections 3 (Watchlists) and 4 (Notifications) with the Phase 5 hardening evidence and the catalogued pending classes for the future Watchlists extraction. - Add three Modulith documents to the README documentation map so the POC, the global plan, and the BC inventory are discoverable from the project entry point.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Incremental groundwork for adopting Spring Modulith across HexaStock. Implements Phases 0–8 of the global refactoring plan (
SPRING-MODULITH-GLOBAL-REFACTORING-PLAN.md) on the experimental branch, without touchingmainand without breaking the Spring-freeapplicationMaven module.Phases delivered
3fe3978@ApplicationModuleonnotifications; documented absence onwatchlists)e82f6b6ModulithVerificationTestexpanded from 4 → 6 tests, scoped viaMODULE_PACKAGES3cabb828e041e73b41a06NotificationDestinationProviderSPI + composite resolvere698ffbCompositeNotificationRecipientResolvera931f9ddf73e27./mvnw clean verify→ BUILD SUCCESS, 60 testsArchitectural decisions
applicationmodule remains Spring-free (ADR-007).@ApplicationModulelives on the consumer side (adapters-outbound-notification);watchlists/package-info.javadocuments why the annotation is intentionally absent.NotificationDestinationProviderinterfaceLoggingNotificationDestinationProvider(always-on)TelegramNotificationDestinationProvider(@Profile("telegram-notifications"))CompositeNotificationRecipientResolver(channel-agnostic, replaces deletedInMemoryNotificationRecipientResolver)MODULE_PACKAGES = {"watchlists", "notifications"}; legacy packages catalogued inMODULITH-BOUNDED-CONTEXT-INVENTORY.md, to be promoted as they migrate.Verification
HexagonalArchitectureTeststill passes.ModulithVerificationTestenforces:notifications → {watchlists}andwatchlists → ∅.Known limitations / out of scope
Portfolio,Market Data, andReportingBCs still live in legacy packages — inventoried only.MarketDataPortrename toStockPriceProviderdeferred (touches every adapter).Next PR (planned)
Extract Portfolio into
portfolios.*per inventory §1, then promote it inMODULE_PACKAGESand tightenModulithVerificationTest.