Blog

EDN+ Server Redevelopment — Rebuilding with Spring MVC and Serving Ads to Karrot

Sharing the process of redeveloping Gmarket's external ad network EDN+ server from a Node.js legacy to a Spring MVC (Servlet Container) base, and integrating product+ad data serving for external placements like Karrot and Danawa.

Spring MVCRedisLocal CacheAd SystemGmarketKarrot

Note: The code in this article has been conceptually rewritten based on actual work experience. It is not associated with the actual company code.

What is EDN+?

EDN+ (Ebay Display Network AD) is Gmarket's external display advertising network. Like Google GDN or Criteo, it is a product that exposes Gmarket CPC ads on external media placements such as Karrot (Danggeun Market) and Danawa.

When an external media source sends an ad request, the EDN+ API combines ad data and product data to return a response. The banner ads you see on the Karrot app are served through this API.


Why We Redeveloped It

The existing EDN+ server was built with Node.js. As the AdTech team moved to unify our tech stack based on Spring, EDN+ was also scheduled for migration.

The redevelopment direction was simple: Wrap the exact behaviors of the existing Node.js server with Spring MVC (Servlet Container) while improving the performance of the external media serving API.


System Architecture

par [Ad Query] [Product/MiniShop Query] Load Ad Area Ad Request (user_id, placement) Fetch CPC Ad Data Ad Info Fetch MiniShop Info Cache Hit (or Internal API Call) Product API Query (max 60ms timeout) Product Info Merge Ad + Product + MiniShop Data Ad Response Banner Ad Exposure Karrot App User Karrot Server EDN+ API (Spring MVC) Redis (Ad Data) Internal Product API Local Cache

Ad Serving Logic: Fixed CPC, Fetched from Redis

The ads served by EDN+ were fixed as CPC ads. It did not require a complex targeting selection engine; the structure simply involved querying and returning pre-selected ad data from Redis.

@RestController
@RequestMapping("/api/v1/edn")
public class EdnAdController {

    @GetMapping("/ads")
    public AdResponse getAd(
        @RequestParam String placementCode,
        @RequestParam(required = false) String userId
    ) {
        // Query CPC Ad Data from Redis
        AdData ad = adRedisRepository.findByPlacement(placementCode);
        if (ad == null) {
            return AdResponse.empty();
        }

        // Combine Product + MiniShop Data
        ProductData product = productService.getProductData(ad.getProductId());

        return AdResponse.of(ad, product);
    }
}

The Karrot Integration Challenge: Product API Latency

Karrot required a response within a maximum of 200ms for ad requests. Ad data was retrieved quickly from Redis, but the problem was the internal Product API.

In addition to ad data, Karrot also required MiniShop (seller information) data. The internal Product API responds with this data included, but it frequently took over 100ms at the p99 percentile. To respond within 200ms, the Product API had to finish within a maximum of 60ms, accounting for EDN+'s own processing time.

Solution 1: Setting a Product API Timeout

We set a 60ms timeout for the Product API calls.

@Service
public class ProductService {

    private static final Duration PRODUCT_API_TIMEOUT = Duration.ofMillis(60);

    public ProductData getProductData(String productId) {
        try {
            return productApiClient.get()
                .uri("/products/{id}", productId)
                .retrieve()
                .bodyToMono(ProductData.class)
                .timeout(PRODUCT_API_TIMEOUT)
                .block();
        } catch (TimeoutException e) {
            log.warn("Product API timeout for productId: {}", productId);
            return ProductData.empty(); // Return empty data on timeout
        }
    }
}

Solution 2: Improving Static Data Performance with Local Cache

MiniShop information or basic meta information does not change frequently. Querying the internal API for this data on every request is wasteful.

We applied a Caffeine-based local in-memory cache.

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)  // 5-minute TTL
            .maximumSize(1000)
        );
        return manager;
    }
}

@Service
public class MiniShopService {

    @Cacheable(value = "miniShop", key = "#shopId")
    public MiniShopData getMiniShopInfo(String shopId) {
        return miniShopApiClient.get(shopId); // Call API only on cache miss
    }
}

Results

  • Successfully transitioned from Node.js legacy to New Spring MVC Server.
  • Stable operation of the integrated serving API for external placements like Karrot and Danawa.
  • Reduced dependency on internal APIs and stabilized response times by applying local caching.

Conclusion

The core lesson from this project was designing internal dependency timeouts by working backwards from the external partner's SLA. Starting from the 200ms required by Karrot, and subtracting our server's processing time, the allowable time for the Product API was naturally defined. If the internal system cannot respond within that time, supplementing it with local caching or a strict timeout fallback is the most realistic approach.