Tradsys is a program that simulates a gamified trading feature for an investment app and allow users to:
- Add new assets.
- Simulate trading of assets (buy/sell).
- Earn gems based on trading activities.
- Follow a ranking system that tracks users with the highest gem count.
- 1 gem per trade (buy or sell)
- 5 bonus gems at 5 trades
- 10 bonus gems at 10 trades
- Bonus gems of trade count at every 20 trades thereafter (20, 40, etc.)
- 3 gems for every consecutive 3 trades, reset if trade is performed by another user
- Bonus gems equal to streak count when streak ≥ 7
This application is built using Spring Boot 3.5.0 and java 24.
# Clone the repository
git clone https://github.com/TPriime/tradesys.git
# Navigate to project directory
cd tradesys
# Run the application
./gradlew src:infrastructure:bootRunThe application should now be available at: http://localhost:8080
Asset simulation rate can be updated by setting the env variable SIMULATION_ASSET_CHANGE_SECS.
For example the following would make asset values to change every 10 seconds (default):
export SIMULATION_ASSET_CHANGE_SECS=10
./gradlew src:infrastructure:bootRun
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/assets |
Fetch all assets |
GET |
/api/trades |
Fetch trades all trades |
GET |
/api/trades?userId={userId} |
Fetch trades by user |
GET |
/api/users |
Fetch all uers |
GET |
/api/users/{userId} |
Fetch a user |
GET |
/api/users/leaderboard |
Fetch top ranking users |
GET |
/api/users/1/stats |
Fetch user stats |
POST |
/api/trades/buy |
Perform trade |
POST |
/api/users |
Create a user |
Creates a user with an initial fund of 1000.00 units for trading activities.
POST /api/users
{
"username": "username"
}POST /api/trade
{
"userId": 1,
"assetId": 1,
"quantity": "2.65"
}This project follows the Hexagonal architecture design style. Hexagonal Architecture separates core business logic from external systems by using interfaces (ports) and implementations (adapters). This promotes flexibility, testability, and clean code boundaries.
.
└── src
├─── domain
│ └── src/main/java/com/agbafune/tradesys
│ ├── api
│ ├── event
│ ├── exception
│ ├── model
│ ├── repository
│ └── simulator
└── spring
└── src/main/java/com/agbafune/tradesys
├── event
├── repository
└── web- domain implements the core business logic of the system. It defines both inbounds and outbound ports to be implemented by the infrastructure.
- api defines outbound ports i.e interfaces consumed by the infrastructure.
- event defines the two major events
TradeEventandUserRewardedEvent. It also defines inbound ports — interfaces consumed by the core. - model defines the domain models.
- repository defines repositories as inbound ports. The infrastruce implements these interfaces to provide a datastore for the application.
- simulator contains an
AssetSimulatorthat simulates live changes in asset prices. It updates the prices of all assets in the system.
- spring is implementation of the infrastructure based on Spring boot. It contains the infrastructure code — defines the actual respositories and provides a web interface for interacting with the system.
- event defines adapters which are concrete implementations of the event ports in the core.
- repository defines adapters which are concrete implementations of the repository ports in the core. In this case — an in-memory database. (Moved to inmemdb)
- web provides a spring based web interface, APIs for interacting with the system.
UserRankManager is a service that tracks user rankings based on the number of "gems" they've earned. It reacts to reward events and dynamically maintains an in-memory ranking system with efficient concurrent updates and lookups. It optimises for efficient rank retrieval under high concurrency writes.
- Rank = (Number of users with higher gems) + 1
- Listens for
UserRewardedEventand updates a user’s gem value. - Maintains fast lookup for:
- A user’s current rank.
- The top N ranked users.
- Ensures thread safety using
ReadWriteLockand concurrent collections.
userGemValue: Maps each user ID to their current gem count.gemValueCounts: Tracks how many users have each gem count (sorted), using aConcurrentSkipListMap.updateRank(...): Adjusts gem values and maintains totals.getRank(...): Calculates a user's rank by counting how many users have more gems.getTopRankedUsers(topN): Retrieves the top N users, assigning equal rank to those with equal gem values.
UserRankManager subscribes to AppEventListener and reacts when a user is rewarded:
eventListener.onUserRewardedEvent(event -> {
updateRank(event.userId(), event.userGems());
});- Read-write operations on rank data are protected by
ReentrantReadWriteLockfor consistency in a concurrent environment. - Efficient use of
ConcurrentSkipListMapfor ordered rankings. - Use of
LongAdderavoids contention when incrementing counts and scales well in concurrent write heavy environments (unlike AtomicLong). - Clean separation of responsibilities through
UserRankAccessorinterface.
LongAdderis more optimized for heavy write environments and where the total sum is retrieved less, sacrificing exact snapshot accuracy for eventual consistency. Therefore if the leaderboard report is much frequently requested under heavy writes, the results may not be up to date.- Memory-bound scalability All user rank data is stored in-memory using ConcurrentHashMap and ConcurrentSkipListMap. As the user base grows, memory usage could spike, especially for long-lived services.
- Real-time accuracy trade-offs Since rankings depend on current gem counts, any delay in event delivery (e.g. missed or late UserRewardedEvent) can skew results.
Modular, clean design with Hexagonal architecture makes it easy to build untop of different infrastructures without touching the core business logic. See an exmaple with using Vert.x
- Enable users manage multiple portfolios.
- If real time leaderboard data can be ignored, implementing a cache would improve performance.