-
Notifications
You must be signed in to change notification settings - Fork 0
sequence diagrams
Source-controlled Mermaid diagrams that reflect the current production Spring Boot 3 backend
under backend/src/main/java/com/group7/backend/. Each diagram is grounded in a real controller,
service, repository, or @TransactionalEventListener-annotated component; no intermediate classes
have been invented. Side-effect topology (AFTER_COMMIT, @Async, @Scheduled, STOMP,
REQUIRES_NEW) is called out with Note over so the reader can distinguish synchronous RPC
ordering from deferred, decoupled, or scheduled work.
Happy-path onboarding of a mentee/mentor pair. The mentee browses ranked mentors via
MatchingController.getTopMentors, opens a request through
MentorshipRequestController.createRequest, the mentor accepts via
MentorshipRequestController.acceptRequest (which delegates to MentorshipService.acceptRequest
and cancels every other pending request from the same mentee), the pair sets the shared goal
through MentorshipController -> MentorshipService.setSharedGoal, and the mentor creates the
first milestone via MilestoneController -> MilestoneService.createMilestone. The
MentorshipPreconditions.requireSharedGoal guard throws GoalRequiredException (HTTP 409
GOAL_REQUIRED) when a mentor tries to create a milestone before the shared goal is set.
sequenceDiagram
actor Mentee
participant MWC as MenteeWebClient
participant MC as MatchingController
participant MS as MatchingService
participant Pipe as MentorScoringPipeline
participant MRC as MentorshipRequestController
participant MRS as MentorshipRequestService
participant BS as BanService
participant CP as MentorshipCooldownPolicy
participant MReqRepo as MentorshipRequestRepository
participant NEP as NotificationEventPublisher
actor Mentor
participant MtWC as MentorWebClient
participant MShipS as MentorshipService
participant MRepo as MentorshipRepository
participant ALog as MentorshipAuditLogRepository
participant MilC as MilestoneController
participant MilS as MilestoneService
participant MilRepo as MilestoneRepository
%% 1. Discovery
Mentee->>MWC: open "Find a Mentor"
MWC->>+MC: GET /api/matching/mentors?keyword&page&size
MC->>+MS: getTopMentors(menteeId, keyword, ..., pageable)
Note over MS: TransactionTemplate (readOnly) opens
MS->>MS: loadEligibleMentee(menteeId)
MS->>MS: rankMentorsFor(mentee, keyword, ...)
MS->>+Pipe: rank(rawCandidates, mentee, slots, ...)
Pipe-->>-MS: List<MentorMatchResponse> (scored, sliced to page)
Note over MS: read-only tx closes BEFORE LLM prose attach
MS-->>-MC: Page<MentorMatchResponse>
MC-->>-MWC: 200 OK Page<MentorMatchResponse>
%% 2. Request creation
Mentee->>MWC: pick mentor, click "Request"
MWC->>+MRC: POST /api/mentorship-requests {mentorId, message}
MRC->>+MRS: createRequest(menteeId, dto)
MRS->>+BS: getActiveBan(menteeId)
BS-->>-MRS: Optional.empty()
alt mentee is banned
BS-->>MRS: Optional<Ban>
MRS-->>MRC: throw UserBannedException
MRC-->>MWC: 403 BANNED_UNTIL {expiresAt, reason}
end
MRS->>CP: assertNotInCooldown(mentorId, menteeId)
MRS->>MReqRepo: existsByMentee_IdAndMentor_IdAndStatus(PENDING)
alt duplicate pending request
MRS-->>MRC: throw MentorshipRequestException
MRC-->>MWC: 409 "already have a pending request"
end
MRS->>MReqRepo: save(MentorshipRequest{status=PENDING})
MReqRepo-->>MRS: saved
MRS->>NEP: publishRequestReceived(mentorId, menteeFirstName)
MRS->>NEP: publishRequestSubmitted(menteeId, mentorFirstName)
MRS-->>-MRC: MentorshipRequestResponse
MRC-->>-MWC: 201 Created
%% 3. Mentor accepts
Mentor->>MtWC: open "Received Requests", click Accept
MtWC->>+MRC: PUT /api/mentorship-requests/{id}/accept {duration}
MRC->>+MShipS: acceptRequest(mentorId, requestId, dto)
MShipS->>MReqRepo: findById(requestId)
MReqRepo-->>MShipS: MentorshipRequest (PENDING)
MShipS->>CP: assertNotInCooldown(mentorId, menteeId)
MShipS->>MShipS: validate duration in {1,3,6}, capacity, no active mentor
MShipS->>MReqRepo: cancelOtherPendingRequests(menteeId, requestId)
Note over MShipS,MReqRepo: side effect: every other PENDING request<br/>from this mentee flips to CANCELLED
MShipS->>MRepo: save(Mentorship{status=ACTIVE, startDate=now, endDate=now+duration})
MShipS->>ALog: save(MentorshipAuditLog ACTIVE)
MShipS->>NEP: publishRequestAccepted(menteeId, mentorFirstName)
MShipS-->>-MRC: MentorshipResponse
MRC-->>-MtWC: 200 OK MentorshipResponse
%% 4. Set shared goal
Mentor->>MtWC: open mentorship, type goal, Save
MtWC->>+MShipS: setSharedGoal(userId, mentorshipId, dto) [via MentorshipController]
MShipS->>MShipS: findForParticipant + checkActive
MShipS->>MRepo: save(mentorship.sharedGoal = dto.sharedGoal)
MShipS-->>-MtWC: 200 OK MentorshipResponse
%% 5. First milestone (GOAL_REQUIRED gate)
Mentor->>MtWC: open Roadmap, click "New milestone"
MtWC->>+MilC: POST /api/mentorships/{id}/milestones {title, ...}
MilC->>+MilS: createMilestone(mentorshipId, mentorId, dto)
MilS->>MRepo: findById(mentorshipId)
MilS->>MilS: checkMentorshipActive(mentorship)
MilS->>MilS: MentorshipPreconditions.requireSharedGoal(mentorship)
alt sharedGoal is null/blank
MilS-->>MilC: throw GoalRequiredException
MilC-->>MtWC: 409 {code: "GOAL_REQUIRED", mentorshipId}
end
MilS->>MilRepo: save(Milestone{status=PENDING})
MilS->>NEP: publishMilestoneCreated(menteeId, title)
MilS-->>-MilC: MilestoneDetailResponse
MilC-->>-MtWC: 201 Created
Cancellation flows are funneled through MentorshipRequestService.cancelOwnPendingRequest, which
flips the request to CANCELLED and calls BanService.recordCancellation. Once cancelCount
crosses BanProperties.cancellationThreshold, BanService writes a fresh Ban row whose
duration follows the escalating-doubling policy capped at maxBanHours. A subsequent
POST /api/mentorship-requests is short-circuited by the same BanService.getActiveBan gate
inside MentorshipRequestService.createRequest with HTTP 403 BANNED_UNTIL. The admin lifts the
ban via BanAdminController.liftBan -> BanService.liftBan, which fires publishBanLifted.
sequenceDiagram
actor Mentee
participant MC as MenteeClient
participant MRC as MentorshipRequestController
participant MRS as MentorshipRequestService
participant MReqRepo as MentorshipRequestRepository
participant BS as BanService
participant BRepo as BanRepository
participant MRepo2 as MenteeRepository
participant NEP as NotificationEventPublisher
actor Admin
participant AC as AdminClient
participant BAC as BanAdminController
%% 1. Cancel pending request — threshold not yet crossed
Mentee->>MC: click "Cancel" on pending request
MC->>+MRC: DELETE /api/mentorship-requests/{id}
MRC->>+MRS: cancelOwnPendingRequest(menteeId, requestId)
MRS->>MReqRepo: findById(requestId)
MReqRepo-->>MRS: MentorshipRequest (must be owner, PENDING)
MRS->>MReqRepo: save(request.status = CANCELLED)
MRS->>+BS: recordCancellation(menteeId, reason)
BS->>MRepo2: findById(menteeId)
BS->>MRepo2: save(mentee.cancelCount += 1)
alt cancelCount < cancellationThreshold
BS-->>MRS: Optional.empty() (warning only)
else cancelCount >= threshold
BS->>BRepo: countNonLiftedByUserId(menteeId) → ordinal
BS->>BRepo: save(Ban{expiresAt=now+computedHours, source=MENTEE_CANCELLATION})
BS->>NEP: publishUserBanned(menteeId, expiresAt, reason, banCount)
BS-->>-MRS: Optional<Ban>
end
MRS-->>-MRC: void
MRC-->>-MC: 204 No Content
%% 2. Retry: next POST short-circuits on ban gate
Mentee->>MC: try to request a new mentor
MC->>+MRC: POST /api/mentorship-requests {mentorId}
MRC->>+MRS: createRequest(menteeId, dto)
MRS->>+BS: getActiveBan(menteeId)
BS->>BRepo: findActive(menteeId, now)
BRepo-->>BS: Optional<Ban>
BS-->>-MRS: Optional<Ban> (present)
MRS-->>-MRC: throw UserBannedException(ban)
MRC-->>-MC: 403 BANNED_UNTIL {expiresAt, reason}
Note over MRS,BS: ban gate runs FIRST so the 403 carries<br/>expiresAt without touching mentor/capacity state
%% 3. Admin lifts the ban
Admin->>AC: open "Bans for user", click Lift
AC->>+BAC: DELETE /api/admin/bans/{banId}
BAC->>+BS: liftBan(banId, adminId)
BS->>BRepo: findById(banId)
alt ban already lifted
BS-->>BAC: ban (no-op, no notification)
else first lift
BS->>BRepo: save(ban.liftedAt=now, ban.liftedByAdminId=adminId)
BS->>NEP: publishBanLifted(userId)
end
BS-->>-BAC: Ban
BAC-->>-AC: 200 OK BanResponse
POST /api/feed/posts is hit through FeedPostController.create. The RateLimitFilter
consumes the feed-post-create bucket (USER key, capacity 30/hr by default) before reaching
the controller. FeedPostService.create rejects Admin requesters, calls
HashtagNormalizer.normalize to lowercase / strip leading # / dedupe, runs
resolveAttachments (size cap 4, uploader-must-match-author provenance check, image-only
content-type allow-list), persists the parent + children in one @Transactional boundary, and
publishes a FeedPostCreatedEvent through FeedPostEventPublisher. The
@TransactionalEventListener(AFTER_COMMIT) plus @Async on FeedFanoutListener.onFeedPostCreated
snapshots followers via FollowRepository.findFollowerIdsByFolloweeId and broadcasts
FeedPostPushPayload to each follower's STOMP topic.
sequenceDiagram
actor User
participant Cli as Client
participant RLF as RateLimitFilter
participant FPC as FeedPostController
participant FPS as FeedPostService
participant URepo as UserRepository
participant HN as HashtagNormalizer
participant ARepo as AttachmentRepository
participant FRepo as FeedPostRepository
participant FEP as FeedPostEventPublisher
participant FFL as FeedFanoutListener
participant FollowRepo as FollowRepository
participant STOMP as SimpMessagingTemplate
User->>Cli: write post, attach 0..4 images, Submit
Cli->>+RLF: POST /api/feed/posts {body, hashtags[], attachmentIds[], lang}
RLF->>RLF: consume "feed-post-create" bucket (USER key)
alt bucket exhausted
RLF-->>Cli: 429 Too Many Requests
end
RLF->>+FPC: forward
FPC->>+FPS: create(authorId, body, rawHashtags, attachmentIds, lang)
FPS->>URepo: findById(authorId)
alt author instanceof Admin
FPS-->>FPC: throw AccessDeniedException
FPC-->>Cli: 403 "Admins cannot create feed posts"
end
FPS->>+HN: normalize(rawHashtags)
HN-->>-FPS: Set<String> normalised
FPS->>FPS: resolveAttachments(ids, authorId)
loop for each attachmentId
FPS->>ARepo: findById(id)
FPS->>FPS: assert uploader == authorId AND imageContentType
end
FPS->>FRepo: save(FeedPost)
FPS->>FRepo: persist hashtag + attachment children (cascade on commit)
FPS->>FEP: publishCreated(saved, author)
FEP->>FEP: ApplicationEventPublisher.publishEvent(FeedPostCreatedEvent)
Note over FPS,FEP: still inside @Transactional —<br/>event is buffered until commit
FPS-->>-FPC: FeedPostResponse
FPC-->>-Cli: 201 Created
Note over FPS,FFL: TX commits → AFTER_COMMIT listeners released
par async fanout
FFL->>+FFL: onFeedPostCreated(event) [@Async]
FFL->>FollowRepo: findFollowerIdsByFolloweeId(authorId)
FollowRepo-->>FFL: Set<Long> followers
loop for each followerId
FFL->>STOMP: convertAndSend(/topic/feed.<id>, FeedPostPushPayload)
end
FFL-->>-FFL: log delivered count
end
FeedReadController.forYou calls FeedReadService.forYouFeed, which oversamples up to
app.feed.forYou.candidate-window (default 200) recent posts via
FeedPostRepository.findForYouCandidates, ranks them through the injected FeedRanker
(@Primary implementation is InterestOverlapFeedRanker; when wired, the advanced path
ForYouScoringPipeline runs MMR + diversity floor + bandit slots), applies the
UserKeywordMuteService filter, batches counts via FeedInteractionService.batchCounts, and
maps through FeedPostMapper.toListItems. Engagement (like / share / comment / repost) is
recorded by FeedInteractionService, which publishes a FeedEngagementEvent inside the
write transaction. FeedEngagementBanditListener.onFeedEngagement listens with
@TransactionalEventListener(AFTER_COMMIT) + @Transactional(propagation=REQUIRES_NEW) —
the dual-connection use is the reason the prod Hikari pool was bumped from 10 to 20.
sequenceDiagram
actor Viewer
participant Cli as Client
participant FRC as FeedReadController
participant FRS as FeedReadService
participant FPRepo as FeedPostRepository
participant FRanker as FeedRanker (InterestOverlapFeedRanker / ForYouScoringPipeline)
participant KMS as UserKeywordMuteService
participant FIS as FeedInteractionService
participant FPMap as FeedPostMapper
participant FELB as FeedEngagementBanditListener
participant TS as ThompsonSamplingService
participant BTab as BanditAlphaTable
%% 4a. READ
Viewer->>Cli: open For-You tab
Cli->>+FRC: GET /api/feed/for-you?page&size
FRC->>+FRS: forYouFeed(viewerId, pageable)
FRS->>FPRepo: findForYouCandidates(viewerId, candidateWindow=200)
FPRepo-->>FRS: List<FeedPost> (up to 200 recent)
FRS->>FRS: buildRankingContext(viewerId) [interests + followee ids + now]
loop for each candidate
FRS->>FRanker: score(post, context)
FRanker-->>FRS: FeedScoreResult{score, factors}
end
FRS->>FRS: Schwartzian sort + slice page
FRS->>+KMS: filter(viewerId, sliced posts)
KMS-->>-FRS: List<FeedPost> visible
FRS->>+FIS: batchCounts(visiblePostIds)
FIS-->>-FRS: Map<postId, PostCounts>
FRS->>+FPMap: toListItems(visible, viewerId, counts, factors)
FPMap-->>-FRS: List<FeedPostListItem>
FRS-->>-FRC: Page<FeedPostListItem>
FRC-->>-Cli: 200 OK Page
%% 4b. ENGAGEMENT (like) → AFTER_COMMIT + REQUIRES_NEW bandit update
Viewer->>Cli: tap "Like" on post
Cli->>+FIS: POST /api/feed/posts/{id}/like
Note over FIS: outer @Transactional opens (Hikari connection #1)
FIS->>FPRepo: requireVisiblePost(postId)
FIS->>FIS: upsertLike(postId, userId)
FIS->>FIS: publishEngagement(post, viewerId)
Note over FIS: ApplicationEventPublisher.publishEvent(FeedEngagementEvent{viewerId, postHashtags})
FIS->>FIS: publishEngagementChanged(post) → FeedPostEngagementChangedEvent
FIS-->>-Cli: 200 FeedPostInteractionState
Note over FIS,FELB: outer TX commits, connection #1 returned
par AFTER_COMMIT bandit hop
FELB->>+FELB: onFeedEngagement(event)
Note over FELB: opens NEW transaction (Hikari connection #2)<br/>REQUIRES_NEW so bandit failure cannot poison the like
FELB->>TS: recordEngagement(viewerId, postHashtags)
TS->>BTab: UPSERT alpha += 1 for each hashtag
TS-->>FELB: void
FELB-->>-FELB: log (warns + swallows on failure)
end
FeedReadController.search enforces "at least one of q, hashtag, since, until, lang is
required" (else 400). The service lowercases + LIKE-escapes the keyword at the boundary so
% and _ cannot act as wildcards, normalises the single hashtag through
HashtagNormalizer.normalize, validates the date window, runs the pg_trgm-accelerated
FeedPostRepository.searchPosts native query, then applies UserKeywordMuteService.filter
after the SQL fetch — which is why the response can carry fewer than size items even
when there are more matches on the next page.
sequenceDiagram
actor Viewer
participant Cli as Client
participant FRC as FeedReadController
participant FRS as FeedReadService
participant HN as HashtagNormalizer
participant FPRepo as FeedPostRepository
participant KMS as UserKeywordMuteService
participant KMRepo as UserKeywordMuteRepository
participant FIS as FeedInteractionService
participant FPMap as FeedPostMapper
Viewer->>Cli: type query, optional #hashtag, Search
Cli->>+FRC: GET /api/feed/search?q&hashtag&since&until&lang&page&size
FRC->>+FRS: search(keyword, hashtag, since, until, lang, viewerId, pageable)
alt all five filters null/blank
FRS-->>FRC: throw IllegalArgumentException
FRC-->>Cli: 400 "at least one of q, hashtag, since, until, lang"
end
FRS->>FRS: normalisedKeyword = escapeLikePattern(lowercased trimmed q)
Note over FRS: escapes \\, %, _ so user input cannot become a wildcard
FRS->>+HN: normalize([hashtag])
HN-->>-FRS: normalisedHashtag (or null)
alt hashtag supplied but failed normalisation
FRS-->>FRC: Page.empty(pageable)
end
FRS->>FRS: validate since < until, window bounds (-10y .. +1d)
FRS->>+FPRepo: searchPosts(normKeyword, normHashtag, since, until, lang, pageable)
Note over FPRepo: hits idx_feed_posts_body_trgm (GIN) + feed_post_hashtags join
FPRepo-->>-FRS: Page<FeedPost> (SQL-filtered, AND-ed)
FRS->>+KMS: filter(viewerId, page.content)
KMS->>KMRepo: countByUserId(viewerId)
alt no mutes
KMRepo-->>KMS: 0
KMS-->>FRS: posts unchanged
else has mutes
KMS->>KMRepo: findByUserIdOrderByCreatedAtAsc(viewerId)
loop for each post
KMS->>KMS: drop if body lowercased contains any muted keyword
end
KMS-->>FRS: filtered (may be SHORTER than size)
end
deactivate KMS
alt filtered empty
FRS-->>FRC: PageImpl([], pageable, page.totalElements)
end
FRS->>+FIS: batchCounts(filteredIds)
FIS-->>-FRS: counts map
FRS->>+FPMap: toListItems(filtered, viewerId, counts)
FPMap-->>-FRS: List<FeedPostListItem>
FRS-->>-FRC: Page<FeedPostListItem> (may have < size items)
FRC-->>-Cli: 200 OK Page
MentorPairMessageController.send resolves (or lazily creates) the mentor-pair Conversation
via ConversationService.findOrCreateForMentorPair, then delegates to MessageService.send.
The feed-post-create-style edge filter consumes the mentor-pair-message-send bucket
(USER key, capacity 60/min). MessageService.send enforces participant + uploader-must-equal-
sender provenance, persists, fires the MessageSentEvent, and the
@TransactionalEventListener(AFTER_COMMIT) on MessageBroadcastListener.onMessageSent
re-fetches with findByIdForBroadcast and STOMP-broadcasts to /topic/conversation/{id}. The
recipient's subsequent PATCH /read lands in MessageService.markAllRead.
sequenceDiagram
actor Sender
participant SCli as SenderClient
participant RLF as RateLimitFilter
participant MPMC as MentorPairMessageController
participant CS as ConversationService
participant MS as MessageService
participant ARepo as AttachmentRepository
participant PartRepo as ConversationParticipantRepository
participant MRepo as MessageRepository
participant AEP as ApplicationEventPublisher
participant NEP as NotificationEventPublisher
participant MBL as MessageBroadcastListener
participant STOMP as SimpMessagingTemplate
participant Recipient as RecipientClient
Sender->>SCli: type message, attach file (optional), Send
SCli->>+RLF: POST /api/conversations/mentor-pair/{otherMentorId}/messages
RLF->>RLF: consume "mentor-pair-message-send" bucket (USER key, 60/min)
alt bucket exhausted
RLF-->>SCli: 429 Too Many Requests
end
RLF->>+MPMC: forward
MPMC->>+CS: findOrCreateForMentorPair(senderId, otherMentorId)
CS-->>-MPMC: Conversation (existing or new)
MPMC->>+MS: send(senderId, conversationId, request)
Note over MS: @Transactional opens
MS->>MRepo: loadAndAuthorize(conversationId, senderId)
MS->>PartRepo: existsByConversationIdAndUserId
alt attachmentId provided
MS->>ARepo: findById(attachmentId)
alt attachment.uploader != sender
MS-->>MPMC: throw ProfileNotVisibleException
MPMC-->>SCli: 403
end
end
MS->>MRepo: save(Message{sentAt=now})
MRepo-->>MS: saved
MS->>PartRepo: findOtherParticipantUserIds(conversationId, senderId)
MS->>AEP: publishEvent(MessageSentEvent{messageId, conversationId, senderId, recipientId})
loop for each other participant
MS->>NEP: publishNewMessage(recipientId, senderFirstName)
end
MS-->>-MPMC: MessageResponse
MPMC-->>-SCli: 201 Created
Note over MS,MBL: TX commits → AFTER_COMMIT listener fires
MBL->>+MBL: onMessageSent(event)
MBL->>MRepo: findByIdForBroadcast(messageId) [eager fetch sender + conv + attach]
MBL->>STOMP: convertAndSend("/topic/conversation/" + conversationId, MessageResponse)
STOMP-->>Recipient: WebSocket frame
deactivate MBL
%% Read receipt
Recipient->>+MPMC: PATCH /api/conversations/mentor-pair/{otherMentorId}/messages/read
MPMC->>+CS: findOrCreateForMentorPair(recipientId, otherMentorId)
CS-->>-MPMC: Conversation
MPMC->>+MS: markAllRead(recipientId, conversationId)
MS->>MRepo: markAllAsReadForReader(conversationId, recipientId, now)
MRepo-->>MS: updated rows count
MS-->>-MPMC: int
MPMC-->>-Recipient: 204 No Content
MentorshipAutoCompletionScheduler (@Scheduled(cron=…), hourly UTC by default,
@ConditionalOnProperty app.mentorships.auto-completion.enabled) fires
MentorshipAutoCompletionService.autoCompleteExpired. The sweep query
(MentorshipRepository.findActiveExpiredAt(ACTIVE, now)) runs outside any transaction; each
matched mentorship is processed inside its own REQUIRES_NEW transaction via a
TransactionTemplate so a single failing row cannot poison the rest of the sweep.
completeOne flips status to COMPLETED, stamps terminatedAt, releases capacity, writes an
audit row, and fires publishMentorshipAutoCompleted to both the mentor and the mentee.
sequenceDiagram
participant Cron as Spring TaskScheduler
participant Sch as MentorshipAutoCompletionScheduler
participant Svc as MentorshipAutoCompletionService
participant MRepo as MentorshipRepository
participant ARepo as MentorshipAuditLogRepository
participant NEP as NotificationEventPublisher
actor Mentor
actor Mentee
Cron->>+Sch: tick (cron "0 0 * * * *", zone UTC)
Sch->>+Svc: autoCompleteExpired()
Svc->>MRepo: findActiveExpiredAt(ACTIVE, now) [no tx]
MRepo-->>Svc: List<Mentorship> expired
alt empty
Svc-->>Sch: 0
end
loop for each expired Mentorship m
Note over Svc: TransactionTemplate.execute (REQUIRES_NEW)
Svc->>Svc: completeOne(m.id, now)
Svc->>MRepo: findById(m.id)
alt status changed since sweep (raced by a manual /end)
Svc-->>Svc: return (skip)
else still ACTIVE
Svc->>MRepo: save(mentorship.status=COMPLETED, terminatedAt=now, terminatedByUserId=null)
Svc->>Svc: release mentee.activeMentorId + mentor.currentMenteeCount
Svc->>ARepo: save(MentorshipAuditLog ACTIVE→COMPLETED "Auto-completed at end_date")
Svc->>NEP: publishMentorshipAutoCompleted(mentorId, menteeFirstName)
Svc->>NEP: publishMentorshipAutoCompleted(menteeId, mentorFirstName)
end
alt RuntimeException
Note over Svc: per-row tx rolls back; sweep continues
end
end
Svc-->>-Sch: processed count
Sch-->>-Cron: void (logs if > 0)
Note over NEP,Mentor: downstream NotificationEventListener<br/>persists Notification row and pushes FCM
NEP-->>Mentor: in-app + push "Mentorship completed"
NEP-->>Mentee: in-app + push "Mentorship completed"
ReportController.submit calls ReportService.createReport, which validates target existence,
rejects self-reports for USER targets, and INSERTs the row. Duplicate active reports are
deduped by the Postgres partial-unique index uq_reports_active_per_reporter_target — the
service catches the DataIntegrityViolationException, inspects the cause for the index name, and
translates only that specific case to DuplicateReportException (HTTP 409). On success the
service fires ReportSubmittedEvent; ReportFanoutListener (@TransactionalEventListener AFTER_COMMIT + @Async) reads userRepository.findAllAdminIds() and publishes a
REPORT_RECEIVED notification to every admin (description is deliberately not forwarded —
PII discipline). The admin transitions the report via AdminReportController.transition ->
ReportService.updateStatus. If the moderator decides a ban is warranted they POST
/api/admin/users/{userId}/ban against AdminController, which delegates to
BanService.imposeAdminBan.
sequenceDiagram
actor Reporter
participant RCli as ReporterClient
participant RC as ReportController
participant RS as ReportService
participant URepo as UserRepository
participant MRepo as MentorshipRepository
participant FPRepo as FeedPostRepository
participant RRepo as ReportRepository
participant AEP as ApplicationEventPublisher
participant RFL as ReportFanoutListener
participant NEP as NotificationEventPublisher
actor Admin
participant ACli as AdminClient
participant ARC as AdminReportController
participant AC as AdminController
participant BS as BanService
%% 1. Submit
Reporter->>RCli: open report dialog on a post, submit
RCli->>+RC: POST /api/reports {targetType=POST, targetId, problemType, description}
RC->>+RS: createReport(reporterId, dto)
alt self-report on USER target
RS-->>RC: throw SelfReportException (400)
end
alt target=POST
RS->>FPRepo: findByIdAndDeletedAtIsNull(targetId)
alt missing
RS-->>RC: throw ResourceNotFoundException (404)
end
else target=USER
RS->>URepo: existsById(targetId)
else target=MENTORSHIP
RS->>MRepo: findById(targetId) + participant check
end
RS->>RRepo: save(Report{status=OPEN, createdAt=now})
alt DataIntegrityViolation on uq_reports_active_per_reporter_target
RS-->>RC: throw DuplicateReportException (409)
end
RS->>URepo: findById(reporterId) → firstName
RS->>AEP: publishEvent(ReportSubmittedEvent{reportId, reporterId, reporterFirstName, targetType})
Note over RS: NO description in the event — PII discipline
RS-->>-RC: ReportResponse
RC-->>-RCli: 201 Created
Note over RS,RFL: TX commits → @Async AFTER_COMMIT
RFL->>+RFL: onReportSubmitted(event)
RFL->>URepo: findAllAdminIds()
loop for each adminId
RFL->>NEP: publishReportReceived(adminId, reporterFirstName, targetType)
end
RFL-->>-RFL: log fanout
%% 2. Admin resolves
Admin->>ACli: open queue, click "Resolve"
ACli->>+ARC: PATCH /api/admin/reports/{id} {status: RESOLVED}
ARC->>+RS: updateStatus(reportId, adminId, RESOLVED)
RS->>RRepo: findById(reportId)
RS->>RS: validateTransition(OPEN|UNDER_REVIEW → RESOLVED)
alt invalid transition (terminal source)
RS-->>ARC: throw InvalidReportTransitionException (400)
end
RS->>RRepo: save(report.status=RESOLVED, reviewedAt=now, reviewedById=adminId)
Note over RRepo: @Version bumps; concurrent admin → 409
RS-->>-ARC: ReportResponse
ARC-->>-ACli: 200 OK
%% 3. Optional ban (separate admin action)
opt Admin decides to ban offender
Admin->>ACli: click "Ban user"
ACli->>+AC: POST /api/admin/users/{userId}/ban {reason, durationHours}
AC->>+BS: imposeAdminBan(userId, adminId, reason, durationHours)
BS->>BS: assert durationHours > 0
BS->>URepo: findById(userId) + findById(adminId)
BS->>BS: BanRepository.countNonLiftedByUserId + 1 → ordinal
BS->>BS: BanRepository.save(Ban{source=ADMIN, expiresAt=now+hours})
BS->>NEP: publishUserBanned(userId, expiresAt, reason, banCount)
BS-->>-AC: Ban
AC-->>-ACli: 200 OK BanResponse
end
Team Members
- Lab 1 Report (12/02/2026)
- Lab 2 Report (19/02/2026)
- Lab 3 Report (26/02/2026)
- Lab 4 Report (05/03/2026)
- Lab 5 Report (12/03/2026)
- Lab 6 Report (26/03/2026)
- Lab 7 Report (02/04/2026)
- Lab 8 Report (18/04/2026)
- Lab 9 Report (30/04/2026)
- Lab 10 Report (07/05/2026)
- Weekly Meeting Notes Template
- Lab Meeting 1 (12.02.2026)
- Weekly Meeting 1 (16.02.2026)
- Weekly Meeting 2 (24.02.2026)
- Weekly Meeting 3 (04.03.2026)
- Weekly Meeting 4 (11.03.2026)
- Weekly Meeting 5 (23.03.2026)
- Weekly Meeting 6 (29.03.2026)
- Weekly Meeting 7 (11.04.2026)
- Weekly Meeting 8 (28.04.2026)
- Weekly Meeting 9 (10.05.2026)
- Use Case Diagram 1 (New Mentor User for Mobile Scenario)
- Use Case Diagram 2 (Mentor-Mentee Matching Scenario)
- Use Case Diagram 3 (New Mentee User Scenario)
- Final Use Case Diagram
- MVP Use Case Diagram
- All Sequence Diagrams
- Sequence Diagram: Mentee Matching
- Sequence Diagram: Mentor Matching
- Sequence Diagram: Mentorship Management
- Sequence Diagram: Registration
- Sequence Diagram: Cancelling Mentorship Relationship and Auto Ban
- Sequence Diagram: Login-Logout
- Sequence Diagram: Reporting-User
- Sequence Diagram: Mentor Profile Management
- MVP Sequence Diagrams
- Test Plan & Coverage (MVP)
- Acceptance Testing Strategy
- Acceptance Tests
- Test Data Strategy
- Web Frontend Test Report
- Amin Abu-Hilga
- Övgü Su Afşar
- Muhammet Sami Çakmak
- Beratcan Doğan
- İbrahim Kayan
- Burak Ögüt
- Mehmet Bora Sarıoğlu
- Future Work (reference, not a deliverable)