Advanced HarmonyOS Game Architecture: How to Split Store and System the Right Way

For HarmonyOS game development, this article presents a four-layer split across Store, System, Engine, and UI to solve state bloat, logic coupling, and scalability issues. Core takeaway: Store holds state only, System implements rules only, and Engine handles orchestration. Keywords: HarmonyOS, game architecture, state management.

Technical Specification Snapshot

Parameter Description
Target Platform HarmonyOS / HarmonyOS Mini Games
Primary Language ArkTS / TypeScript
Architectural Style State-driven + domain-based separation + orchestration layer
Core Principles / Patterns Single responsibility, stateless System, centralized orchestration
Star Count Not provided in the source
Core Dependencies ArkUI, Store state container, System business modules

Architecture illustration AI Visual Insight: This image serves as a structural overview of the article’s theme. It highlights the layered relationship between game state, rule systems, and UI presentation, making it a good entry-point diagram for architectural decomposition rather than a concrete API screenshot.

Animation illustration AI Visual Insight: This animated image works more like a dynamic process demo. It communicates the idea that state changes drive visual updates, helping developers understand that the UI is not the center of game logic, but a visual projection of state.

Basic layering breaks down quickly as HarmonyOS games become more complex

Many projects start with only three layers: Store, System, and UI. At first, that feels simple enough. But once you add skills, levels, drops, AI, and cross-device synchronization, the problems appear immediately: Store turns into a giant object, Systems start calling each other, and the UI is forced to patch in business logic.

The real issue is not that ArkUI is unsuitable for complex games. The issue is that many teams still use a page-oriented mindset to build what is actually a systems engineering problem. Once a game reaches medium complexity, it is no longer a collection of pages. It is an evolving state system.

You should define the most important boundary first

Store is the data layer and should only hold current state. System is the behavior layer and should only modify state according to rules. UI should only subscribe to state and render it. Any cross-layer overreach eventually becomes maintenance cost.

// GameStore.ts
export class GameStore {
  playerHp: number = 100 // Player HP
  enemyHp: number = 100 // Enemy HP
  level: number = 1 // Current level
  exp: number = 0 // Current experience
  gold: number = 0 // Current gold
}

This code turns Store into a pure state container and prevents it from carrying business rules.

Store loses control when state and rules live in the same class

Early in development, many teams put attack, levelUp, dropItem, and enemyAI directly into GameStore. That speeds up short-term delivery, but it destroys long-term maintainability because one class now owns state, flow control, decision logic, and side effects at the same time.

In essence, this approach compresses the entire game into a God Object. The usual outcome is a triple failure: hard to test, hard to extend, and hard to collaborate on. Once one feature changes, a chain of unrelated logic can break with it.

Proper decomposition starts with single responsibility

Store should manage state and basic reads and writes, but it should not carry complex rules. System should execute behaviors, modify state, and drive flow, but it should not persist long-lived state itself. This is the first principle of decomposition.

// Anti-pattern: putting logic into Store
class BadGameStore {
  enemyHp: number = 100

  attackEnemy() {
    this.enemyHp -= 10 // The business rule is directly coupled to the state object
  }
}

This code shows the anti-pattern: once a state object starts handling rules, it almost always continues to bloat.

Strengthening System is how you keep Store lightweight

Once you move logic into System, state and rules naturally decouple. System does not own data. It only operates on the Store passed into it, which makes it reusable, composable, and testable. It behaves more like a set of pure business capability modules.

Combat rules should live in an independent System

// system/BattleSystem.ts
import { GameStore } from '../store/GameStore'

export class BattleSystem {
  attack(store: GameStore) {
    store.enemyHp -= 10 // Apply the attack rule
    if (store.enemyHp <= 0) {
      store.enemyHp = 0 // Keep state within valid bounds
    }
  }

  takeDamage(store: GameStore) {
    store.playerHp -= 5 // Apply the damage rule
    if (store.playerHp <= 0) {
      store.playerHp = 0 // Clamp HP at zero
    }
  }
}

This code extracts combat rules from the state container and turns them into an independent, stateless business module.

System should be split by domain, not by file growth

When BattleSystem gets larger, do not keep stuffing more logic into it. Split it by domain into BattleSystem, LevelSystem, DropSystem, and AISystem. The criterion is not “there is too much code.” The criterion is “the responsibilities are now different.”

The value of domain-based decomposition is that each System has a clear boundary: combat owns damage and resolution, leveling owns experience and level progression, drops own probability and rewards, and AI owns decision-making and behavior selection.

A single directory reveals whether your architecture is healthy

// system/
// ├── BattleSystem.ts   // Combat rules
// ├── LevelSystem.ts    // Leveling rules
// ├── DropSystem.ts     // Drop rules
// └── AISystem.ts       // AI decision rules

This code shows the structural boundaries created by domain modeling, which makes team collaboration and module expansion much easier.

Leveling rules should exist as an independent unit

// system/LevelSystem.ts
import { GameStore } from '../store/GameStore'

export class LevelSystem {
  addExp(store: GameStore, exp: number) {
    store.exp += exp // Increase experience
    if (store.exp >= 100) {
      store.level += 1 // Level up when the threshold is reached
      store.exp = 0 // This example uses a reset-to-zero strategy
    }
  }
}

This code encapsulates progression logic as an independent rule unit so it does not mix with combat logic.

Adding an orchestration layer is what turns multiple Systems into a stable flow

As the number of Systems grows, a new question appears: who decides execution order? If the UI directly calls battle, level, and drop in sequence, the presentation layer becomes the business orchestration center again.

That is why you need an Engine as an orchestration layer. It does not hold core business state, and it does not implement concrete rules. Instead, it drives the overall flow, manages ordering, and isolates the UI from Systems.

GameEngine is the only recommended coordinator between Systems

import { GameStore } from '../store/GameStore'
import { BattleSystem } from './system/BattleSystem'
import { LevelSystem } from './system/LevelSystem'

class GameEngine {
  battle = new BattleSystem()
  level = new LevelSystem()

  update(store: GameStore) {
    this.battle.takeDamage(store) // Process damage first
    this.level.addExp(store, 1) // Then process experience gain
  }
}

const store = new GameStore()
const engine = new GameEngine()

setInterval(() => {
  engine.update(store) // Centralized orchestration for the game loop
}, 16)

This code uses Engine to connect multiple Systems and establish a stable game update loop.

Preventing Systems from calling each other is a key step in controlling coupling

If BattleSystem directly calls LevelSystem, and LevelSystem then calls DropSystem, the dependency graph quickly spirals out of control. In the end, no module can be tested independently, and one change can affect the entire system.

The correct approach is to let Engine orchestrate all collaboration. Each System should care only about the inputs and outputs of its own domain and should not actively depend on other Systems. That is how the architecture stays flat, clear, and predictable.

The final recommended four-layer structure

// UI -> Engine -> System -> Store
// UI: presentation layer, only responds to state changes
// Engine: orchestration layer, manages execution order
// System: rules layer, encapsulates domain logic
// Store: state layer, holds the single source of truth

This code summarizes an extensible HarmonyOS game architecture with the smallest possible model.

This decomposition directly solves four core problems

First, complexity becomes manageable. Each System can evolve independently instead of forcing all logic into one class. Second, the architecture is extensible. You can continue adding quests, inventory, skills, and achievements without breaking the backbone.

Third, the design becomes testable. To test BattleSystem, you only need to pass in a mockStore, without depending on the UI. Fourth, the architecture is naturally friendly to multiple clients. Because Store is the single source of truth and System is a pure logic layer, multiple UI front ends can work around the same state model.

The most common developer mistakes all come from responsibility leakage

Mistake one: Store fills up with logic and becomes a God class. Mistake two: System holds state, for example by caching data long-term through this.store, which creates lifecycle confusion. Mistake three: UI adds business patches and starts owning flow decisions.

Once these signs appear, your architectural boundaries have already weakened. The fix is not to keep adding helper functions. The fix is to return to clear responsibility boundaries: state, rules, orchestration, and presentation should each stay in their proper place.

What you are really building is a state transformation system, not a collection of pages

At the advanced stage, you need a mindset shift: game development is not about “building a few feature pages.” It is about defining the mapping between inputs, rules, state changes, and rendered results. Clicks, AI decisions, and timers are all input sources. System performs the transformations, Store records the results, and UI renders them automatically.

That is also the key to sustainable evolution in HarmonyOS game architecture. As long as the layering stays clear, the system can remain stable as business complexity grows, instead of collapsing under temporary logic.

FAQ

1. Why should Store not contain methods like attack or levelUp?

Because that turns the state object into a business object, coupling state, rules, and flow together. It may save time in the short term, but it destroys testability and extensibility over time.

2. Why should System not hold a reference to Store?

Once System owns state, it introduces lifecycle and synchronization issues. This becomes especially hard to control in multi-page or multi-client scenarios. Stateless Systems are easier to reuse, compose, and test.

3. Will GameEngine become a new “big class”?

No, as long as Engine only handles orchestration and does not carry concrete business rules. Its job is to coordinate execution order, not to replace BattleSystem or LevelSystem in implementing details.

Core summary: This article restructures the core architecture of HarmonyOS games as complexity grows: let Store hold state only, let System focus on rule execution, and use GameEngine for centralized orchestration. This solves state bloat, system coupling, and UI-level business logic leakage.