Tech Stack
Spring Boot 4.0.3 Modern Spring Boot framework with Java 17
Spring WebFlux Reactive WebClient for non-blocking HTTP calls
Caffeine Cache High-performance in-memory caching
Maven Build and dependency management
Project Structure
source/Back/crafter/
├── src/main/java/com/crafter/league/of/legends/crafter/
│ ├── config/
│ │ ├── CacheConfig.java # Caffeine cache configuration
│ │ ├── CorsConfig.java # CORS settings
│ │ └── WebClientConfig.java # WebClient setup
│ ├── controller/
│ │ ├── GameController.java # Game API endpoints
│ │ └── ItemController.java # Item endpoints
│ ├── service/
│ │ ├── DataDragonService.java # Data Dragon integration
│ │ └── GameService.java # Game logic
│ ├── model/
│ │ ├── Item.java # Item entity
│ │ └── ItemsData.java # API response wrapper
│ └── dto/
│ ├── GameQuestion.java # Game question DTO
│ ├── ValidationRequest.java # Validation request DTO
│ └── ValidationResponse.java # Validation response DTO
├── src/main/resources/
│ └── application.properties # Configuration
├── pom.xml # Maven dependencies
└── Dockerfile # Container definition
Key Dependencies
From pom.xml:
< properties >
< java.version > 17 </ java.version >
</ properties >
< dependencies >
<!-- Spring Boot Web for REST APIs -->
< dependency >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-web </ artifactId >
</ dependency >
<!-- Spring WebFlux for reactive WebClient -->
< dependency >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-webflux </ artifactId >
</ dependency >
<!-- Spring Cache abstraction -->
< dependency >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-cache </ artifactId >
</ dependency >
<!-- Caffeine for high-performance caching -->
< dependency >
< groupId > com.github.ben-manes.caffeine </ groupId >
< artifactId > caffeine </ artifactId >
</ dependency >
<!-- Lombok for reducing boilerplate -->
< dependency >
< groupId > org.projectlombok </ groupId >
< artifactId > lombok </ artifactId >
</ dependency >
<!-- Validation API -->
< dependency >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-validation </ artifactId >
</ dependency >
</ dependencies >
Configuration Layer
Cache Configuration
Caffeine cache setup in config/CacheConfig.java:
@ Configuration
@ EnableCaching
public class CacheConfig {
public static final String ITEMS_CACHE = "items" ;
public static final String CRAFTABLE_ITEMS_CACHE = "craftableItems" ;
@ Bean
public CacheManager cacheManager () {
CaffeineCacheManager cacheManager = new CaffeineCacheManager (
ITEMS_CACHE,
CRAFTABLE_ITEMS_CACHE
);
cacheManager . setCaffeine ( Caffeine . newBuilder ()
. maximumSize ( 500 )
. expireAfterWrite ( 24 , TimeUnit . HOURS )
. recordStats ());
return cacheManager;
}
}
Cache Strategy : Items are cached for 24 hours with a maximum of 500 entries. This reduces Data Dragon API calls and improves response times.
WebClient Configuration
Reactive HTTP client setup in config/WebClientConfig.java:
@ Configuration
public class WebClientConfig {
@ Value ( "${ddragon.base.url}" )
private String baseUrl ;
@ Value ( "${webclient.timeout.connect:5000}" )
private int connectTimeout ;
@ Value ( "${webclient.timeout.read:10000}" )
private int readTimeout ;
@ Value ( "${webclient.buffer.size:5242880}" )
private int bufferSize ; // 5 MB default
@ Bean
public WebClient webClient () {
HttpClient httpClient = HttpClient . create ()
. option ( ChannelOption . CONNECT_TIMEOUT_MILLIS , connectTimeout)
. responseTimeout ( Duration . ofMillis (readTimeout))
. doOnConnected (conn ->
conn . addHandlerLast ( new ReadTimeoutHandler (readTimeout, TimeUnit . MILLISECONDS ))
. addHandlerLast ( new WriteTimeoutHandler (readTimeout, TimeUnit . MILLISECONDS )));
// Configure buffer for large Data Dragon responses
ExchangeStrategies strategies = ExchangeStrategies . builder ()
. codecs (configurer -> configurer . defaultCodecs (). maxInMemorySize (bufferSize))
. build ();
return WebClient . builder ()
. baseUrl (baseUrl)
. clientConnector ( new ReactorClientHttpConnector (httpClient))
. exchangeStrategies (strategies)
. build ();
}
}
Buffer Size : Set to 5MB to handle large JSON responses from Data Dragon API (typically 500KB+).
CORS Configuration
Cross-origin setup in config/CorsConfig.java:
@ Configuration
public class CorsConfig implements WebMvcConfigurer {
@ Value ( "${cors.allowed.origins}" )
private String [] allowedOrigins ;
@ Override
public void addCorsMappings ( CorsRegistry registry ) {
registry . addMapping ( "/**" )
. allowedOriginPatterns (allowedOrigins)
. allowedMethods ( "GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" )
. allowedHeaders ( "*" )
. allowCredentials ( true )
. maxAge ( 3600 );
}
}
Service Layer
DataDragonService
Core service for fetching and caching item data from service/DataDragonService.java:
@ Slf4j
@ Service
@ RequiredArgsConstructor
public class DataDragonService {
private final WebClient webClient ;
@ Value ( "${ddragon.version}" )
private String version ;
@ Value ( "${ddragon.language}" )
private String language ;
@ Value ( "${ddragon.base.url}" )
private String baseUrl ;
@ Cacheable ( value = CacheConfig . ITEMS_CACHE , key = "'all'" )
public Map < String , Item > fetchAllItems (){
log . info ( "Fetching items from Data Dragon API - Version: {}, Language: {}" ,
version, language);
String url = String . format ( "/cdn/%s/data/%s/item.json" , version, language);
ItemsData itemsData = webClient . get ()
. uri (url)
. retrieve ()
. bodyToMono ( ItemsData . class )
. block ();
if (itemsData == null || itemsData . getData () == null ) {
log . error ( "Failed to fetch items from Data Dragon" );
throw new RuntimeException ( "Failed to fetch items from Data Dragon API" );
}
// Enrich items with full image URLs
Map < String , Item > enrichedItems = itemsData . getData (). entrySet (). stream ()
. collect ( Collectors . toMap (
Map . Entry :: getKey,
entry -> {
Item item = entry . getValue ();
item . setId ( entry . getKey ());
// Build full image URL
if ( item . getImage () != null && item . getImage (). containsKey ( "full" )) {
String imageName = (String) item . getImage (). get ( "full" );
String fullImageUrl = String . format ( "%s/cdn/%s/img/item/%s" ,
baseUrl, version, imageName);
item . setImageUrl (fullImageUrl);
}
return item;
}
));
log . info ( "Successfully fetched and cached {} items" , enrichedItems . size ());
return enrichedItems;
}
@ Cacheable ( value = CacheConfig . CRAFTABLE_ITEMS_CACHE , key = "'craftable'" )
public Map < String , Item > fetchCraftableItems () {
log . info ( "Filtering craftable items" );
Map < String , Item > allItems = fetchAllItems ();
Map < String , Item > craftableItems = allItems . entrySet (). stream ()
. filter (entry -> {
Item item = entry . getValue ();
// Item is craftable if it has components and is not basic
return item . getFrom () != null &&
! item . getFrom (). isEmpty () &&
item . getTotalCost () > 0 ;
})
. collect ( Collectors . toMap ( Map . Entry :: getKey, Map . Entry :: getValue));
log . info ( "Found {} craftable items out of {} total items" ,
craftableItems . size (), allItems . size ());
return craftableItems;
}
}
Spring’s @Cacheable automatically caches method results:
First call : Fetches from Data Dragon API and stores in cache
Subsequent calls : Returns cached data (no API call)
Cache key : Static key 'all' or 'craftable'
Expiration : 24 hours (configured in CacheConfig)
The service enriches items with full CDN URLs: Original: {"full": "1001.png"}
Enriched: "https://ddragon.leagueoflegends.com/cdn/16.3.1/img/item/1001.png"
Items are craftable if:
They have from array (component IDs)
Array is not empty
Total cost is greater than 0
Controller Layer
GameController
REST API endpoints in controller/GameController.java:
@ Slf4j
@ RestController
@ RequestMapping ( "/api/game" )
@ RequiredArgsConstructor
public class GameController {
private final GameService gameService ;
@ GetMapping ( "/question" )
public ResponseEntity < GameQuestion > getQuestion (
@ RequestParam ( required = false , defaultValue = "MEDIUM" ) String difficulty ) {
log . info ( "GET /api/game/question?difficulty={}" , difficulty);
try {
GameQuestion question = gameService . generateQuestion (difficulty);
return ResponseEntity . ok (question);
} catch ( Exception e ) {
log . error ( "Error generating question" , e);
return ResponseEntity . internalServerError (). build ();
}
}
@ PostMapping ( "/validate" )
public ResponseEntity < ValidationResponse > validateAnswer (
@ Valid @ RequestBody ValidationRequest request ) {
log . info ( "POST /api/game/validate - Target: {}, Selected: {}" ,
request . getTargetItemId (), request . getSelectedComponentIds ());
try {
ValidationResponse response = gameService . validateAnswer (request);
return ResponseEntity . ok (response);
} catch ( Exception e ) {
log . error ( "Error validating answer" , e);
return ResponseEntity . internalServerError (). build ();
}
}
}
API Endpoints
Returns a random craftable item with options. Query Parameters:
difficulty (optional): Game difficulty (default: MEDIUM)
Response: {
"targetItem" : { ... },
"options" : [ ... ],
"correctComponents" : [ "1001" , "1036" ]
}
Validates player’s answer. Request Body: {
"targetItemId" : "3031" ,
"selectedComponentIds" : [ "1001" , "1036" ]
}
Response: {
"isCorrect" : true ,
"correctComponents" : [ "1001" , "1036" ]
}
Logging
The application uses SLF4J with Lombok’s @Slf4j annotation:
@ Slf4j
@ Service
public class DataDragonService {
public Map < String , Item > fetchAllItems () {
log . info ( "Fetching items from Data Dragon API - Version: {}, Language: {}" ,
version, language);
// ...
log . info ( "Successfully fetched and cached {} items" , enrichedItems . size ());
}
}
Log output:
2026-03-04 10:30:15 - Fetching items from Data Dragon API - Version: 16.3.1, Language: es_MX
2026-03-04 10:30:16 - Successfully fetched and cached 200 items
Design Patterns
Dependency Injection Spring manages all beans and dependencies via constructor injection with Lombok’s @RequiredArgsConstructor
Repository Pattern DataDragonService acts as a repository for external API data
DTO Pattern Separate DTOs for API requests/responses decouple external contracts from internal models
Configuration Externalization All configuration in application.properties for easy environment-specific changes
Error Handling
Controllers catch exceptions and return appropriate HTTP status codes: try {
GameQuestion question = gameService . generateQuestion (difficulty);
return ResponseEntity . ok (question);
} catch ( Exception e ) {
log . error ( "Error generating question" , e);
return ResponseEntity . internalServerError (). build ();
}
Service throws RuntimeException if API call fails: if (itemsData == null || itemsData . getData () == null ) {
log . error ( "Failed to fetch items from Data Dragon" );
throw new RuntimeException ( "Failed to fetch items from Data Dragon API" );
}
Jakarta Validation annotations on DTOs: public class ValidationRequest {
@ NotBlank
private String targetItemId ;
@ NotEmpty
private List < String > selectedComponentIds ;
}
Next Steps
Data Dragon Integration Deep dive into API integration details
Docker Deployment Learn how to containerize the backend