1. Project Overview and Key Features — a TFT community for party recruitment and chat
TFTgogo is a community service where Teamfight Tactics players can check game information, recruit people to play with, and chat with participants. The user action looks simple, but the service has to check login state, party capacity, existing participation, closed recruitment posts, and chat permissions at the same time. My work focused on keeping party participation and realtime chat state consistent between the screen and the server.
My Contribution
Party participation guards, chat permission boundaries, auth-state UI rules, and SSE recovery evidence.
Decision
Backend service rules should stop invalid joins and writes even if the UI is stale or bypassed.
Verification
Regression tests and code evidence cover stale auth state, duplicate participation, and chat write boundaries.
Limitation
Full multithread load testing for party capacity was not completed, so this is documented as remaining work.
Case 1. Three cache invalidation paths that can be missed
- Failure state
- A deck aggregation commits to the database while stale meta-deck rankings remain visible from cache.
- Root cause
- Pre-commit eviction, exception paths that skip
@CacheEvict, and an async lambda that bypasses the Spring proxy each create a different invalidation gap. - Decision
- Use
TransactionAwareCacheManagerProxyfor post-commit eviction, explicit cleanup in exception and asyncfinallyblocks, andAtomicBoolean.compareAndSetto reject overlapping aggregations. - Code evidence
global/config/CacheConfig.java,domain/deck/service/impl/MetaDeckServiceImpl.java,domain/deck/service/impl/AdminDeckServiceImpl.java- Verification / limit
MetaDeckServiceImplTest.javacovers overlap and flag recovery. Caffeine remains a single-instance cache; multi-instance deployment would need distributed invalidation.
Case 2. Two users racing for the final party seat
- Failure state
- Two requests read the same remaining capacity and both pass validation, exceeding capacity or duplicating participation.
- Root cause
- A plain read-check-update sequence leaves a race window between validation and persistence.
- Decision
- Lock
member → partyin a fixed order withPESSIMISTIC_WRITEinside one transaction, with a three-second lock timeout. - Code evidence
domain/community/service/impl/CommunityPartyServiceImpl.java,domain/community/repository/PartyPostRepository.java,domain/member/repository/MemberRepository.java- Verification / limit
CommunityPartyServiceImplTest.javacovers full parties, duplicate joins, and lock order. A real multithread load test has not yet been run.
Case 3. Same email, different social account ownership
- Failure state
- Email-only matching can merge accounts owned through different identity providers.
- Decision
- Use
provider + socialIdas the ownership key, enforce a composite database constraint, and re-query after a concurrent first-signup conflict. - Code evidence
domain/member/service/impl/MemberServiceImpl.java,domain/member/repository/MemberRepository.java,db/migration/V1__init_schema.sql- Verification
MemberServiceImplTest.javaandSocialMemberCreationServiceTest.javacover provider identity and convergence after unique-constraint conflicts.- Outcome
- The model prioritizes account ownership over convenient but unsafe email-based merging.
Stale auth state
The page checks current authentication before cached participation state can pollute the UI.
from stale cache
checked first
button disabled
no stale action
| Problem | A stale participation value can show joined controls to a logged-out user. |
|---|---|
| Decision | Check the current auth state before participation state. |
| Result | Logged-out users are pinned to the login-required state, even with stale joined data. |
Duplicate participation guard
Duplicate joins are handled as a server-side domain consistency issue before persistence.
party action
for mutation
accepted party
repository.save
| Problem | Concurrent or external requests cannot be stopped by disabled buttons alone. |
|---|---|
| Decision | Lock the member row and check active posts/participations before saving. |
| Result | The duplicate path exits before save is called. |
Chat permission boundary
Community visibility stays open, while message-writing responsibility stays tied to an authenticated user.
public
principal required
unauthorized
keep token
| Problem | Locking reads hurts visibility, but open writes remove user accountability. |
|---|---|
| Decision | Keep GET/SSE reads public and require authentication for POST writes. |
| Result | Users can read openly, while writes remain traceable to an authenticated identity. |
SSE lifecycle
Realtime failure is modeled as retry, terminal failure, or auth expiry instead of one generic error.
connected
query cache
max 3
failed recovery
| Problem | Disconnects, retries, auth expiry, and recovery failure can all look like one error. |
|---|---|
| Decision | Separate retry eligibility and delay into a reconnect policy. |
| Result | The UI can distinguish recoverable failures from terminal failures. |