The kingdom is under attack. Hundreds of goblins are advancing toward the castle walls, and the royal defenders are preparing for battle.
Some enemies are fast scouts carrying small weapons. Others are heavily armored brutes. Each goblin has its own position and health, but many share the same appearance, attack strength, and basic characteristics.
The game engine could create a completely independent object containing every characteristic of every enemy. But when an army contains thousands of similar creatures, repeatedly storing identical information becomes unnecessary.
Today’s PHP Design Patterns Game introduces Flyweight, a structural pattern designed to share common state among many objects while preserving the information that makes each object unique.
The problem: thousands of similar enemies
Imagine a Goblin class containing a name, sprite description, attack strength, position, and current health.
Every new scout repeats the same name, appearance information, and attack strength. Only its position and health differ.
In a small game, this duplication may be harmless. In a larger simulation, particularly when shared definitions contain substantial data, the repeated information can become expensive.
Flyweight separates the common information from the information belonging to each individual enemy.
Intrinsic and extrinsic state
The Flyweight pattern distinguishes between two categories of state.
Intrinsic state is information that can be shared safely. In our game, it includes the goblin type, symbol, and attack strength.
Extrinsic state belongs to a particular goblin. Its position and current health must remain independent because those values can change during combat.
The design will store intrinsic state in immutable GoblinType objects. Individual Goblin objects will hold their unique state and reference a shared type.
Step 1: Define the shared goblin type
We begin with a readonly class representing the characteristics shared by enemies of the same type.
<?php
declare(strict_types=1);
final readonly class GoblinType
{
public function __construct(
public string $name,
public string $symbol,
public int $attack
) {}
public function describe(int $x, int $hp): string
{
return sprintf(
'%s at %d: %s HP=%d ATK=%d',
$this->symbol,
$x,
$this->name,
$hp,
$this->attack
);
}
}The readonly declaration is available in PHP 8.2 and prevents normal reassignment of the object’s properties after initialization.
This matters because several goblins will reference the same GoblinType instance. Changing its characteristics unexpectedly would affect every enemy sharing that instance.
Step 2: Create a Flyweight factory
The factory ensures that equivalent goblin definitions reuse the same object.
final class GoblinTypeFactory
{
/** @var array<string, GoblinType> */
private array $types = [];
public function get(
string $name,
string $symbol,
int $attack
): GoblinType {
$key = json_encode(
[$name, $symbol, $attack],
JSON_THROW_ON_ERROR
);
return $this->types[$key] ??=
new GoblinType($name, $symbol, $attack);
}
public function count(): int
{
return count($this->types);
}
}The factory uses a deterministic representation of the type’s characteristics as its cache key.
If a matching type already exists, the factory returns it. Otherwise, it creates and stores a new GoblinType.
The factory is not responsible for managing individual goblin health or positions. Its responsibility is sharing type definitions.
Step 3: Create independent enemies
Each goblin receives a reference to its shared type while maintaining its own combat state.
final class Goblin
{
public function __construct(
public readonly GoblinType $type,
public readonly int $x,
public int $hp
) {}
public function show(): string
{
return $this->type->describe(
$this->x,
$this->hp
);
}
public function hit(int $damage): void
{
$this->hp = max(
0,
$this->hp - $damage
);
}
}The Goblin object represents one enemy on the battlefield. Its health can change independently, even when several enemies share the same GoblinType.
This distinction is the central idea behind Flyweight: shared characteristics do not imply shared identity or shared mutable state.
Step 4: Defend the castle
Our game now creates two goblin types and three individual enemies.
$factory = new GoblinTypeFactory();
$scout = $factory->get('Scout', 's', 3);
$brute = $factory->get('Brute', 'B', 7);
$army = [
new Goblin($scout, 2, 10),
new Goblin($scout, 4, 10),
new Goblin($brute, 7, 20),
];
$army[1]->hit(4);
foreach ($army as $goblin) {
echo $goblin->show(), PHP_EOL;
}
echo 'Shared types: ',
$factory->count(),
PHP_EOL;
echo 'Same scout type: ',
$army[0]->type === $army[1]->type
? 'yes'
: 'no',
PHP_EOL;Expected output
s at 2: Scout HP=10 ATK=3
s at 4: Scout HP=6 ATK=3
B at 7: Brute HP=20 ATK=7
Shared types: 2
Same scout type: yesAlthough the army contains three enemies, the factory maintains only two shared type objects.
The two scouts reference the same GoblinType instance. However, damaging the second scout does not change the first scout’s health.
The demonstration is intentionally small. It establishes object sharing, not a measured memory-performance improvement.
Why this is Flyweight rather than ordinary caching
Flyweight does use a cache-like factory, but caching alone does not define the pattern.
The important architectural decision is separating reusable intrinsic state from individual extrinsic state.
The factory makes sharing practical by ensuring that equivalent definitions use the same object. Individual goblins retain their own positions, health, and other changing properties.
If the game simply cached complete mutable Goblin objects, different enemies could accidentally share health and other combat state. That would be incorrect.
When does Flyweight make sense?
Flyweight is most useful when an application maintains large numbers of similar objects containing substantial duplicated information.
Possible applications include simulation engines, document-processing systems, graphical editors, and other workloads where many instances share common definitions.
In conventional request-based PHP applications, the benefit may be limited because object lifetimes are often short. Long-running processes and memory-intensive workloads may offer more opportunities.
Developers should measure actual memory usage before introducing the additional abstraction. Sharing tiny objects can add complexity without producing a meaningful benefit.
Reader challenge: The Royal Army
Extend the game with an ArcherGoblin type that uses a different symbol and attack strength.
Create 100 individual archers through the same factory. Verify that they all reference one shared GoblinType object while maintaining independent health values.
For an advanced challenge, compare memory usage between a design that duplicates large immutable type definitions and one that shares them. Use representative workloads and report the actual measurements rather than assuming a particular improvement.
The Goblin Horde demonstrates the purpose of Flyweight: preserve individuality where it matters, and share information where duplication provides no value.

























