Asset Bundle Dependencies: Building a Hierarchical Asset Management System
← Back
3.5.26

Asset Bundle Dependencies: Building a Hierarchical Asset Management System

How we cut manual work and download sizes with a hierarchical system

Lior Orenbach
ByLior Orenbach
Senior Backend Engineer

Background

Asset bundles are a core part of how Papaya Gaming delivers visual content to players. They package images, fonts, buttons, and other UI elements that define the look and feel of tournaments, liveops, and other in-game experiences. Each bundle is versioned, published through an internal backoffice tool (Optimus), and served to clients via CDN.
Until recently, every asset bundle was a standalone entity. There was no formal relationship between bundles — each one was created, configured, and published independently.

The Problem

When the art department needed to customize asset bundles for seasonal events, like Valentine's Day, Christmas, black
friday, etc. The process was manual and repetitive. A single search for "Valentine's" in Optimus returned dozens of
bundles. For each one, artists had to:

  1. Open the bundle individually.
  2. Upload the relevant assets (themed icons, backgrounds, buttons).
  3. Publish a new version.
__wf_reserved_inherit

This created three compounding issues:

  • Repetitive manual work: Artists spent significant time performing the same operation across tens of bundles per event.
  • Inconsistency risk: Because each bundle was updated independently, discrepancies between bundles were common — a missed upload or wrong asset version in one bundle could go unnoticed.
  • Duplicate downloads: Shared assets (such as currency icons or game mode graphics) were duplicated across bundles, increasing the total download size for players.

Evaluating Alternatives

The core requirement was clear: allow asset bundles to share common assets without duplication, while preserving backwards compatibility with the existing system. We considered two approaches:
1. Flat shared asset pools: A global pool of shared assets that any bundle could reference. This would solve duplication but offered no structure — any bundle could reference anything, making it difficult to reason about completeness and consistency.
2. Typed dependency hierarchy: A structured three-tier system where bundles can only reference specific types of other bundles, enforcing a directed, acyclic relationship. This approach offered both deduplication and structural guarantees.

We chose the typed hierarchy for its ability to prevent circular dependencies by design and for providing a clear mental model for the art and operations teams.

Solution Architecture
The dependency system introduces three asset bundle types:

Type Role Can Reference
Skin Player-facing bundle. Referenced by liveops and tournaments. Zero or one Base
Base Common assets for a category of bundles (typically one per asset bundle kind). Multiple Resources (up to a configurable limit)
Resource Basic building blocks — icons, fonts, buttons. Nothing (leaf node)

__wf_reserved_inherit

Design Decisions

  • No circular dependencies: The hierarchy enforces a strict direction — Skin → Base → Resource. A resource cannot reference a base, and a base cannot reference a skin. This is guaranteed by the type system, not by runtime validation alone.
  • Immutable types: The dependency type is set when the asset bundle is created and cannot be changed afterward. This prevents accidental corruption of the dependency graph.
  • One base per skin: A skin can reference at most one base. The art team initially requested support for multiple bases, but we opted for simplicity in the first version. This constraint can be relaxed in a future iteration if use cases emerge.
  • Backwards compatible: All existing asset bundles default to type Skin, preserving current behavior. No migration of existing data is required.

A practical example: for a Valentine's event, the art team creates one Resource bundle containing all shared currency and game mode icons, one Base bundle per asset bundle kind containing the common layout and logic references, and individual Skin bundles for each specific variant. Updating the shared icons requires changing only the Resource bundle — the change propagates to all dependent bases and skins automatically.

Implementation Details

Database Changes

Asset bundles are stored in a document database (not MySQL). The schema changes were minimal:

Asset Bundle Collection - added a single field:

1 dependencyType: "skin" | "base" | "resource"

Asset Bundle Package - added a dependsOnVersions field:

1 dependsOnVersions: string[] // e.g., ["packId@1.0", "packId@2.1"]

Dependencies are defined per version, using the same versioning schema already in place ( packId@major.minor ). This means dependencies can evolve across versions — a new version of a skin can reference a different base than the previous version.

Dependency Resolution

The existing API exposed a function

1 getUrl(assetBundlePackId: string, context: IApiContext): string

that accepted an asset bundle ID and returned a single CDN URL. We preserved this function for backwards compatibility and introduced a new function alongside it:

1 getDownloadableAssetBundle(packId: string, context:
AssetBundleContext): DownloadableAssetBundle[]

This function accepts a skin pack ID and recursively resolves all dependencies:

  1. Look up the requested skin bundle and determine the version to serve.
  2. If the skin references a base, resolve the base bundle and its version.
  3. For each base, resolve all referenced resource bundles.
  4. Return a flat array of DownloadableAssetBundle objects, each containing the full metadata the client needs - URL, dependency type, pack ID, and version.
__wf_reserved_inherit

The resolution reuses the version-selection logic extracted from the original getURL implementation, applied recursively at each level of the hierarchy.

API Changes

The existing assetBundleUrl field on liveop and tournament objects in response to the client remains unchanged.For backwards compatibility a new field was added alongside it to carry the full downloadable asset bundle payload, an array of objects, each with URL, metadata, and dependency type. The client receives a flat array (not a tree structure), but each entry is annotated with its type, allowing the client to handle caching and rendering appropriately.

A conditional check prevents unnecessary overhead: if an asset bundle has no dependencies, the new field is omitted entirely.

Optimus UI Changes
Three additions were made to the backoffice (Optimus) interface:

  1. Dependency type filter: The asset bundle list view can now be filtered by type (Skin, Base, Resource), making it straightforward to locate bundles of a specific tier.
  2. Type indicator on bundle cards: Each asset bundle card displays its dependency type, providing immediate visual identification.
  3. Dependencies panel: In the version detail view, a new expandable section lists all dependencies of the selected version, showing which base and resource bundles it references.
__wf_reserved_inherit

Validation
All existing validations continue to run (completeness checks, PapayaKit version compatibility, game ID verification). On top of these, new dependency-specific validations enforce:

  • Type-consistent references (a skin can only reference a base, a base can only reference resources).
  • Version existence checks for all referenced dependencies.
  • Configurable limits on the number of resource references per base.

A review workflow ensures that newly uploaded assets are not immediately visible to players. Artists can use a cheat code to preview new assets in the client before publishing to production.

Results

Stakeholder Before After
Artists Update each of 30+ bundles individually per event Update one Base or Resource; changes propagate automatically
Operations Manual configuration per bundle, error-prone Simpler configuration with structured dependency model
Players Download duplicate assets across bundles Shared resources cached — reduced download size
Engineering Ad-hoc relationships between bundles Typed, validated dependency graph


Trade-offs and Lessons Learned

  • Single base per skin: This simplification keeps the first version manageable but may not cover all future use cases. We chose to ship a constrained version and iterate based on real usage rather than design for hypothetical scenarios upfront.
  • OS-specific URLs cannot be persisted: Asset bundle URLs are OS-dependent (iOS vs. Android). Any code that resolves asset bundle URLs and stores them in the database must resolve the URL at request time, not at storage time. This is a known pitfall that caught one team early in adoption — storing a URL resolved for iOS and then serving it to an Android client results in a broken asset.
  • Type immutability: Preventing type changes after creation avoids dependency graph corruption but requires teams to plan their bundle structure before creating bundles. In practice, this has not been a friction point, but it is worth noting for teams designing new bundle hierarchies.

Lior Orenbach is a backend developer at Papaya Gaming, working on the asset management and content delivery systems.