Chickens Must Die.Technical docs / Architecture

Authoritative Multiplayer Server Starter

Architecture

A modular, object-oriented server architecture with explicit constructor injection. This document covers the application layers, startup and shutdown lifecycle, room-scoped dependencies, and authoritative movement processing.

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:

  1. GameApplication is the only composition root.
  2. Domain code does not retrieve dependencies through global getters.
  3. The transport layer does not contain gameplay rules.
  4. Infrastructure implements small ports defined in data-access.
  5. Each room has its own systems and handlers, while sharing the initialized game world.
  6. External data is validated before use.
  7. 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

  • GameApplication creates high-level modules and passes their public ports to other modules.
  • GameRuntime loads the map and constructs the shared GameWorld.
  • ServerConfig assembles the typed configuration objects.
  • AppLogger provides Pino, child loggers, and HTTP middleware.

core

  • GameLoop runs the registered systems on each tick.
  • SystemRegister preserves deterministic system order.
  • RoomHandlerRegister registers Colyseus message handlers.
  • RoomRuntimeBuilder assembles 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

  • ColyseusModule declares rooms.
  • createConfiguredGameRoom captures server-side dependencies in a class that Colyseus instantiates later.
  • HttpModule attaches routers to the Express application provided by defineServer().
There is no second listen() call or second Express server.

Composition root

The GameApplication constructor creates the following components in order:

  1. Typed configuration and logger
  2. InfrastructureModule
  3. GameModule
  4. RateLimitModule
  5. HealthModule
  6. ColyseusModule
  7. HttpModule

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:

  1. index.ts → GameApplication: call createServer().
  2. GameApplication → Colyseus/HTTP: call defineServer(config).
  3. Colyseus/HTTP → GameApplication: invoke beforeListen().
  4. GameApplication → GameRuntime: call initialize().
  5. GameRuntime: load and validate the map exactly once, then create collision, spawn, and collider services.
  6. GameApplication → InfrastructureModule: call initialize().
  7. InfrastructureModule: initialize PostgreSQL, then Redis, then RabbitMQ.
  8. 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:

Godot input MoveHandler Zod + local rate limit Player.inputBuffer consumeMovementInput Free / Grid movement CollisionWorld PlayerPositionGuardSystem Colyseus State patch Godot client

System order within a tick

  1. Time synchronization
  2. Selected movement system
  3. Position guard
  4. Seed collection
  5. Player eating
Changing the order changes gameplay behavior and requires regression testing.

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.env outside 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.