Skip to content

Spring Modulith — Phase 0–8 incremental groundwork#28

Draft
alfredorueda wants to merge 26 commits into
mainfrom
feature/experimental-spring-modulith-notifications
Draft

Spring Modulith — Phase 0–8 incremental groundwork#28
alfredorueda wants to merge 26 commits into
mainfrom
feature/experimental-spring-modulith-notifications

Conversation

@alfredorueda

Copy link
Copy Markdown
Owner

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 touching main and without breaking the Spring-free application Maven module.

Strategy: Option C — incremental hybrid. Annotate only already-extracted modules (watchlists, notifications); inventory the rest for follow-up PRs. No Kafka, no microservices split, no Spring Boot upgrade.

Phases delivered

# Phase Commit
0 Baseline + plan committed 3fe3978
1 Module boundaries (@ApplicationModule on notifications; documented absence on watchlists) e82f6b6
2 ModulithVerificationTest expanded from 4 → 6 tests, scoped via MODULE_PACKAGES 3cabb82
3 Portfolio BC inventory 8e041e7
4 Market Data BC inventory 3b41a06
5 Notifications hardening: NotificationDestinationProvider SPI + composite resolver e698ffb
6 Unit tests for CompositeNotificationRecipientResolver a931f9d
7 Documentation alignment (POC, BC inventory, README map) df73e27
8 Final quality gate: ./mvnw clean verify → BUILD SUCCESS, 60 tests

Architectural decisions

  • application module remains Spring-free (ADR-007). @ApplicationModule lives on the consumer side (adapters-outbound-notification); watchlists/package-info.java documents why the annotation is intentionally absent.
  • Notifications boundary hardened with an SPI:
    • NotificationDestinationProvider interface
    • LoggingNotificationDestinationProvider (always-on)
    • TelegramNotificationDestinationProvider (@Profile("telegram-notifications"))
    • CompositeNotificationRecipientResolver (channel-agnostic, replaces deleted InMemoryNotificationRecipientResolver)
  • Modulith verification scoped to MODULE_PACKAGES = {"watchlists", "notifications"}; legacy packages catalogued in MODULITH-BOUNDED-CONTEXT-INVENTORY.md, to be promoted as they migrate.

Verification

./mvnw clean verify
[INFO] BUILD SUCCESS
[INFO] Total time:  01:06 min
  • 60 tests across 10 reactor modules, all green.
  • ArchUnit HexagonalArchitectureTest still passes.
  • ModulithVerificationTest enforces: notifications → {watchlists} and watchlists → ∅.

Known limitations / out of scope

  • Portfolio, Market Data, and Reporting BCs still live in legacy packages — inventoried only.
  • MarketDataPort rename to StockPriceProvider deferred (touches every adapter).
  • No persistence/transactional behaviour added to Notifications (POC scope).

Next PR (planned)

Extract Portfolio into portfolios.* per inventory §1, then promote it in MODULE_PACKAGES and tighten ModulithVerificationTest.

RaimonVelasco02 and others added 26 commits April 22, 2026 20:03
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
…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.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants