Architectural style
The project uses a straightforward modular OOP architecture with
constructor injection. There is no dependency-injection container:
objects are instantiated explicitly, and dependencies can be traced
from GameApplication down to each concrete adapter.
The core rules are:
GameApplicationis the only composition root.- Domain code does not retrieve dependencies through global getters.
- The transport layer does not contain gameplay rules.
-
Infrastructure implements small ports defined in
data-access. - Each room has its own systems and handlers, while sharing the initialized game world.
- External data is validated before use.
- The entry point neither assembles dependencies nor creates a second HTTP server.
Layers
The server source tree is organized by responsibility:
server/src/
app/ composition root, runtime, config, logger
core/ system loop and room registries
game/ GameModule, GameWorld, map repository
features/ domain rules and systems
data-access/ ports and feature-oriented queries/stores
infrastructure/ PostgreSQL, Redis, RabbitMQ adapters
rooms/ room lifecycle and protocol handlers
schema/ state synchronized by Colyseus
shared/ contracts, config, types, validation
transport/ Colyseus and Express adapters
app
-
GameApplicationcreates high-level modules and passes their public ports to other modules. -
GameRuntimeloads the map and constructs the sharedGameWorld. -
ServerConfigassembles the typed configuration objects. -
AppLoggerprovides Pino, child loggers, and HTTP middleware.
core
-
GameLoopruns the registered systems on each tick. -
SystemRegisterpreserves deterministic system order. -
RoomHandlerRegisterregisters Colyseus message handlers. -
RoomRuntimeBuilderassembles the components owned by an individual room.
features
A feature contains business rules. A system implements
ISystem, while a feature implements
IGameFeature and registers the required objects in the
room context. Systems receive dependencies through their
constructors.
data-access and infrastructure
data-access defines the ports used by higher layers,
such as SqlQueryExecutor,
RedisCommandExecutor, and EventPublisher.
infrastructure implements those ports using concrete
libraries.
This boundary makes it possible to test a feature without a real database and to replace an adapter without rewriting domain logic.
transport
ColyseusModuledeclares rooms.-
createConfiguredGameRoomcaptures server-side dependencies in a class that Colyseus instantiates later. -
HttpModuleattaches routers to the Express application provided bydefineServer().
listen() call or second Express
server.
Composition root
The GameApplication constructor creates the following
components in order:
- Typed configuration and logger
InfrastructureModuleGameModuleRateLimitModuleHealthModuleColyseusModuleHttpModule
app.config.ts creates the application and exports the
result of createServer(). index.ts only
calls Colyseus listen(app).
Startup lifecycle
The startup sequence, expressed as steps rather than a Mermaid diagram so it works in a plain browser:
-
index.ts→GameApplication: callcreateServer(). -
GameApplication→ Colyseus/HTTP: calldefineServer(config). -
Colyseus/HTTP →
GameApplication: invokebeforeListen(). -
GameApplication→GameRuntime: callinitialize(). -
GameRuntime: load and validate the map exactly once, then create collision, spawn, and collider services. -
GameApplication→InfrastructureModule: callinitialize(). -
InfrastructureModule: initialize PostgreSQL, then Redis, then RabbitMQ. -
GameApplication→ Colyseus/HTTP: signal readiness; the server begins listening.
The map is ready before the first room is created. Calling
getWorld() before initialization throws a clear error.
If infrastructure startup fails, the application logs the error, closes resources that have already been opened, and does not begin listening.
During shutdown, InfrastructureModule.close() closes
RabbitMQ, the application Redis connection, and the PostgreSQL pool
in parallel. Colyseus Presence/Driver connections belong to the
Colyseus server and remain under its lifecycle.
Object lifetimes
| Scope | Examples |
|---|---|
| Process |
Configuration, logger, InfrastructureModule,
data sources, GameRuntime
|
| Map |
GameWorld, MapCollisionWorld,
SpawnPositionFinder, collider resolver
|
| Room |
State, GameLoop, registries,
systems, handlers, PlayerFactory
|
| Player |
Player, input buffer, client-specific realtime
limits
|
Creating a room with dependency injection
Colyseus instantiates the room class itself. The
createConfiguredGameRoom(runtime, rateLimiter, config,
logger)
factory returns a class extending GameRoom. Its
no-argument constructor calls the base constructor with the
server-side dependencies.
Clients cannot override those dependencies through room options.
Movement message flow
A movement message travels through the following steps:
System order within a tick
- Time synchronization
- Selected movement system
- Position guard
- Seed collection
- Player eating
Dependency direction
Allowed
app → modules → ports ← infrastructure
rooms → features → shared types
transport → application services
Disallowed patterns
- A feature importing a concrete data source.
-
Reading
process.envoutside configuration factories. - A system retrieving the global collision world.
- Passing server-side services through client options.
- Putting SQL queries inside a data source.
- Putting gameplay logic in an HTTP router or transport handler.
Multiple maps
GameRuntime and the repository are designed to map
mapId to a file/world. Currently,
MapId only allows default.
Adding another map requires extending the type, the path map, and world selection when a room is created.