01 Blocks and Block States
Consider the following pairs of blocks:
- a stair facing east and a stair facing west;
- an extended piston and an unextended piston;
- redstone dust with power level 0 and redstone dust with power level 15;
- a waterlogged stair and a normal stair.
Sometimes they look completely different, yet they still belong to the same kind of block. How does Minecraft describe that difference?
The answer is: block and block state are not the same concept.
1 A block is a rule set; a state is its current values
In the source code, the essence of Block is "the shared set of rules for one kind of block". For example, all oak stairs share the same StairsBlock instance, which defines:
- which properties can exist (facing, top/bottom half, shape, whether it is waterlogged);
- how placement orientation is computed;
- what collision shape it has;
- how its shape is adjusted when it receives an NC update;
- which loot table it uses when broken.
However, merely knowing that "this is an oak stair" is still not enough to reconstruct it in the world. The game also needs to know which way it faces, whether it occupies the top or bottom half, whether its shape is straight or a corner, and whether it is waterlogged.
Those current values together form a block state. Therefore, the essential relationship between Block and BlockState is this: Block defines "what this thing can become, and how the rules change after it does"; BlockState records "which values it currently has."
2 Properties and values
A block state is made up of a set of properties (Property) and their values. Every property has three hard constraints:
- Each property provides only a finite set of allowed values —
StairsBlock'shalfcan only betoporbottom; it cannot temporarily become something else. - All combinations of all properties over all allowed values are created in advance — when a
Blockis constructed,StateManagerenumerates every combination and creates an immutableBlockStateobject for each one. - Created states cannot be modified — calling
state.with(property, value)does not mutate the original state in place; instead, it returns anotherBlockStatefrom a prebuilt lookup table.
Take stairs as an example. StairsBlock declares four properties:
Total number of combinations: 4 × 2 × 5 × 2 = 80. All 80 of those BlockState objects are created when the block is registered, and any one of them can then be found with an O(1) table lookup.
Key rule: you cannot temporarily add a new property to a block. A piston's extended can only be true or false; redstone dust's power can only be 0 through 15. Any write outside the allowed value set throws an exception inside State.with().
3 One set of coordinates corresponds to one state
When the game queries a set of coordinates in the world, it returns a BlockState, not just a Block.
The subchunk's PalettedContainer<BlockState> stores block states directly. That way, reading one set of coordinates immediately tells you:
- what block it is (via
BlockState.getBlock()); - what values all of its state properties currently hold (via
BlockState.get(Property)).
This leads to an important property: large numbers of blocks in the world with the same state can all share the same pre-generated BlockState object. Each set of coordinates does not need its own tiny mutable object; they share immutable state instances.
Internally, BlockState holds its owning Block and an immutable property map. Calling:
does not modify the original state. Instead, it retrieves another state from the prebuilt state table. If the specified value is not in the allowed set for that property, the source code throws an exception.
That means many blocks in the world with the same state can share the same pre-generated state object, instead of every coordinate owning its own freely mutable little object.
4 Subchunks and palettes: how states are stored
A world contains millions of coordinate positions. If every position stored its own complete BlockState object, memory usage and storage size would explode quickly. Minecraft solves this with subchunks and palettes.
4.1 Subchunks
A 16×16×16 region is called a subchunk (ChunkSection). A normal chunk can contain up to 24 subchunks (Y = -64 to Y = 319, one layer every 16 blocks). Each subchunk manages 4096 coordinate positions, but it does not store one separate BlockState object per position.
4.2 Palette: using IDs instead of objects
Each subchunk maintains a palette, recording "which block states appear in this subchunk":
- The palette is a list, and each entry is a unique
BlockState. - Each of the 4096 positions in the subchunk stores a small numeric ID pointing to one palette entry.
- When a position's block state needs to be read, the ID is used to look up the corresponding
BlockState. - When a new state is written, if that state is not already in the palette, it is added, and the position stores the new entry's ID.
In the source code, this process is handled by PalettedContainer<BlockState>. It is called "Paletted" precisely because its core is a palette (similar to indexed colors in images) plus an index array. The block data inside a subchunk is really "a table plus 4096 IDs," not "4096 independent BlockState objects."
4.3 Why this matters
This design has several direct consequences that are important for understanding later mechanics:
- Large numbers of blocks with the same state take almost no extra space: a whole stone floor occupies only one entry in the palette, and every position shares the same ID.
- Palette size is limited: if too many block state types appear in a subchunk, the palette upgrades to a larger storage format (such as direct global-palette mapping), but the principle stays the same.
- Writing = changing an ID: changing the block state at one position is, in essence, changing that position's ID to the ID of another state. If the new state is already in the palette, the write is just a cheap ID replacement.
- Shared instances: because the
BlockStateobjects in the palette are pre-generated immutable objects, every position that refers to that state is effectively pointing to the same object.
Once you understand palettes, the bridge between "what the world stores" and "what BlockState.with() does" becomes clear: the world stores IDs, the IDs point to the palette, the palette points to pre-generated immutable BlockState objects, and BlockState.with() returns another pre-generated object.
5 The default state is the starting point for construction
Every block has a default state. At the end of Block construction, it is set to StateManager.getDefaultState() — that is, the first enumerated state combination.
Specific blocks can adjust their default properties in their own constructors. For example, a piston sets its default state to:
The default state does not mean that all newly placed pistons face north. During placement, PistonBlock#getPlacementState starts from the default state, then uses .with() based on the player's look direction to obtain the actual state that should be written to the world. A more accurate description of the default state is: the starting point for constructing other states — all states are reachable from that point through a finite number of .with() calls.
6 What block state can store
Block state is suitable for storing information that:
- has a finite number of possible values;
- is frequently used in block behavior checks;
- needs to be read quickly;
- usually also needs to participate in rendering or synchronization.
Examples include facing, on/off, power level, age, and waterlogged state.
But data such as the 27 inventory slots inside a chest, the text on a sign, or a hopper's cooldown can take many different values. If all of that were encoded as block state, the number of state combinations would explode rapidly.
That kind of data is stored by a block entity (BlockEntity). Block state describes "which finite state this position is currently in," while a block entity attaches more complex, independently changing data to certain positions.
6.1 Block entities are not entities
Although both names contain the word "entity," block entities (BlockEntity) and entities (Entity) are completely different systems:
- Entity has coordinates, motion, and a collision box; it can move and actively participates in per-tick computation. Pigs, minecarts, TNT, and item frames all fall into this category.
- BlockEntity is fixed to a specific set of block coordinates and does not move on its own. The only reason it exists is that the block at that position needs to store or process data beyond what
BlockStatecan represent. Chests, hoppers, signs, and beacons all belong here.
Not every block has a block entity. Whether a position should have one is determined first by the BlockState at that position — for example, the chest block state declares "I need a block entity," and only then does the game create a chest block entity for that coordinate. Block entity data is stored in the chunk's block-entity section using NBT format (see the next section), separately from the palette data inside subchunks.
Most block entities participate in some kind of scheduled computation, but with different periods: hoppers check transfers every 8gt, beacons update effects every 80gt, while signs do not participate in any periodic computation at all. For the detailed behavior of block entities, see Block Entities.
7 NBT: the general format for complex data
As mentioned above, BlockState can only store information with a finite range of values. So where do highly variable data such as the items inside a chest, the text on a sign, or a hopper's cooldown timer go?
The answer is NBT (Named Binary Tag), the tree-structured format Minecraft uses to store complex structured data.
7.1 What NBT is
You can think of NBT as "JSON with types": the data is organized as a tree of key-value pairs, but each value has an explicit type tag (integer, string, list, compound tag, and so on). An NBT tree has a top-level compound tag containing multiple named child tags.
NBT is widely used anywhere complex data needs to be stored:
- Block entities: chest contents, sign text, hopper cooldowns, beacon effects, etc.
- Entities: mob attributes, equipment, potion effects, custom names, etc.
- Items: enchantments, durability, custom names,
BlockStateTag,BlockEntityTag, etc. - Chunk storage: block entity data, entity data, scheduled tick queues, etc.
- Player data: inventory, ender chest, advancements, statistics, etc.
- Level data: world settings, game rules, etc.
7.2 The division of labor between NBT and BlockState
BlockState and NBT solve completely different problems:
BlockStateanswers "which finite state is this position currently in" — the range of values is known, the number of possibilities is controlled, and fast lookup is required.- NBT answers "what other complex data is attached to this position/entity/item" — the range of values is open-ended, nested structures may be involved, and the data does not need to participate in every block behavior check.
So, at the same set of coordinates in the world, there may simultaneously exist:
- one
BlockState(guaranteed by the palette), and - one piece of NBT data (held by the block entity).
The former answers "what block is this, which way does it face, is it on or off"; the latter answers "what is inside the chest, what is written on the sign, how much cooldown is left on the hopper." They have clearly separated responsibilities and do not replace one another.
8 Where fluid state lives
Subchunks do not store another full-size fluid array next to the block state data.
ChunkSection#getFluidState first obtains the BlockState at that position, then calls the block state's getFluidState. Taking stairs as an example:
- when
waterlogged=false, it returns the empty fluid state; - when
waterlogged=true, it returns the still water fluid state.
So at the same set of coordinates, "block state" and "fluid state" are not two completely unrelated pieces of data. Fluid state can be derived from the block state at that coordinate.
9 Why state changes matter
When you pull a lever, open a trapdoor, or change the power of redstone dust, the block type usually does not change. What changes is the BlockState of that same block.
However, the game still needs to write the new state back into the chunk. After that write, it may trigger:
- changes to the model and collision shape;
- lighting checks;
- client synchronization;
- NC updates;
- comparator updates;
- block entity creation, removal, or updates.
Therefore, "it didn't turn into another kind of block" does not mean "the world data didn't change." The next article starts from one concrete write operation and follows how those things happen.
10 Summary
Blockdescribes the rules shared by one kind of block.BlockStatedescribes the finite state combination currently used at one position.- Properties have finite allowed values, and the state manager pre-creates all combinations.
.with(...)returns another pre-generated state; it does not modify the old one in place.- Every set of block coordinates in the world corresponds to one block state.
- Complex and highly variable data is usually stored by block entities.
- Fluid state is derived from the block state at the current position.
11 Source Analysis
11.1 Block: the shared rules of a block kind
The Block constructor reveals the relationship between a block type and its states:
appendProperties is where a subclass declares "which properties this block has." Each subclass overrides it to register its own property set.
11.2 StateManager: enumerating every state combination
The StateManager constructor is responsible for building every possible state for a block:
getDefaultState() returns this.states.get(0) — the first state in the list is the default state.
11.3 State: immutable state lookup
State's entries is an immutable ImmutableMap<Property<?>, Comparable<?>> recording the current value of every property in the current state. get() reads directly from entries; with() uses the prebuilt withTable to perform an O(1) state transition:
with() does not modify the original state. It retrieves and returns another BlockState from the prebuilt state table. Large numbers of blocks in the world with the same state can therefore share the same BlockState object instead of each coordinate storing its own independent state instance.
11.4 Concrete block examples
Stairs (StairsBlock) declares four properties:
FACING has 4 horizontal directions, HALF has 2 values, SHAPE has 5 values, and WATERLOGGED has 2 values, for a total of 4 × 2 × 5 × 2 = 80 states.
Pistons (PistonBlock) declare two properties and adjust the default value in the constructor:
During placement, getPlacementState starts from the default state and computes the actual state to write based on the player's look direction. This demonstrates the pattern of "compute the initial state first, then write it."
Redstone dust (RedstoneWireBlock) declares five properties: POWER (0-15) plus four directional connection properties:
11.5 Fluid state is derived from block state
ChunkSection's getFluidState proves that fluid state is not stored independently:
Take stairs' getFluidState as an example:
When waterlogged=true, it returns water's fluid state; when false, it returns the empty fluid state. So the "block state" and "fluid state" at one set of coordinates are in a derived relationship, not two separately stored pieces of data.
11.6 Block's flag constants
These flags drive all the follow-up behavior after a write in World#setBlockState, which the next article will examine in detail.
11.7 Reference class list
net.minecraft.block.Blocknet.minecraft.block.BlockStatenet.minecraft.state.Statenet.minecraft.state.StateManagernet.minecraft.state.property.Propertynet.minecraft.block.StairsBlocknet.minecraft.block.PistonBlocknet.minecraft.block.RedstoneWireBlocknet.minecraft.world.chunk.ChunkSection
