Core Design Principles
This document explains the post-refactoring design principles; the implementation has converged on them (see the Roadmap), and the landed status of individual items is annotated where it applies.
For concrete runtime semantics, API return values, IDs, registry, payload, timer, Lua VM, ops, and cluster reserved rules, see the Runtime Semantics Decisions.
1. Single node first, cluster optional
Shield must first make the single-node game service runtime stable. Multi-node deployment, service discovery, cluster routing, and node orchestration do not enter shield_core.
shield_cluster is the official optional module for multi-process / multi-machine communication, node heartbeats, remote routing cache, and optional service discovery. It must reuse core's service handle, send/call, timeout, and error semantics; it must not stuff service discovery or cluster orchestration back into core.
The reason for this tradeoff is direct: Shield first makes the core paths — services, messages, networking, timers, hot Lua iteration — thin and stable, then adds cluster capability on clean boundaries, instead of continuing to expand into a generic enterprise framework.
2. Lua first
Game logic is written in Lua by default. C++ is for the runtime, networking, Lua bindings, low-level data access, and a small number of performance-sensitive extensions.
shield.send("room", "join", { player_id = "1001" })
local ok, result = shield.call("player", "get_info", { id = "1001" })Lua business code should not be aware of CAF, the threading model, connection objects, or C++ container details.
3. CAF is an implementation detail
CAF handles the actor machinery: spawn, send, request, schedule, scheduling, and so on. Shield layers game-server semantics on top:
- Named services, not directly exposed CAF actor handles.
send/calldual mode.- Service lifecycle hooks.
- Timers and delayed tasks.
- Lua-friendly table data structures.
The user-facing API does not expose CAF headers.
The one-line principle:
CAF covers the machinery; Shield adds the service semantics.CAF can carry actor scheduling, mailboxes, spawn, send, request, timers, and remote actor transport. Shield still must implement the service name registry, Lua coroutine pending/resume, the Lua service lifecycle, and the shield.* API.
4. Small core
shield_core contains only the Actor/Service semantics:
- Service lifecycle.
- Service naming and lookup.
send/call.spawn/exit.- timer / timeout.
- coroutine pending/resume.
- CAF adapter.
Lua, network, transport, data, config, log, and bootstrap are all official runtime modules around shield_core, not core semantics themselves. cluster, global, ops, player, etc. are official optional modules.
DI, annotations, conditional wiring, plugins, Prometheus, health checks, service discovery, event buses, a standalone protocol layer, and similar pieces of the old architecture are all NOT core. If any must be kept, it needs an explicit non-core home and must not enter core or the default startup main path.
5. Messages are events
Actor messages are the only cross-service communication model. No separate event bus is designed to express the same concept.
shield.event, if it exists, can only be a synchronous observer utility inside the current Lua service / current Lua VM. It does not enter the mailbox, does not serialize payloads, does not cross service/VM/node boundaries, and is not used for application ready, shutdown, or service lifecycle orchestration.
Recommended pattern:
shield.send("chat", "broadcast", {
channel = "world",
text = "hello"
})Use shield.call only when a return value is needed.
6. The gateway is the connection and session binding boundary
The Gateway runtime owns client connections and session binding management; it is not a generic Lua message dispatcher. The fixed chain for ordinary client RPC is:
frame decode -> header.route_id
-> Gateway lightweight routing table check
-> session.target (AuthService or PlayerService)
-> CAF ClientIngress
-> target Service adapter: cached handler
-> body decode
-> concrete Lua RPC handlerRoute information lives only in the wire header; the body only carries the business parameters of that RPC. The Gateway does not parse the target service from the route_id, does no multi-target routing, and does not decode ordinary business bodies.
The CAF behavior distinguishes internal message categories such as ClientIngress, ordinary service send/call, and lifecycle control; the Shield Service adapter maps the header route_id to Lua handlers cached in the target VM at startup.
All client messages go to the target service bound to the session (before login → AuthService, after login → PlayerService). Dynamic routing of room/scene/map is managed by PlayerService private state and does not participate at the Gateway layer.
7. Data access is provided by plugins
Data access does not enter shield_core, and is no longer carried by a separate shield_data module. Backend capabilities such as SQL, document stores, caches, queues, and leaderboards are provided by Plugin System v1; business Lua accesses them through plugin namespaces and binding logical names.
local db = shield.database.mysql("database.default")
local ok, rows = db:query("SELECT * FROM users WHERE id = ?", { id })
local q = shield.queue.redis("queue.events")
q:publish("chat.world", { text = "hello" })Plugins own connection pooling and driver integration; core provides no ORM, generates no complex mappers, and manages no cross-service transactions.
8. Operations and debugging are separated
shield_ops is the official optional operations and debugging layer; it is not part of shield_core.
shield_coreowns semantics.shield_opsowns metrics, health, profile, diagnostics, and the console.shield_opsmay read internal state, but must not depend on core semantics in reverse.- Business code should not call
shield_opsdirectly; it may only use it through management endpoints or the console.