Designing Purpose-Built Terraform Resources: From Generic Blocks to Context-Aware Abstractions
As part of developing a Terraform provider for Minecraft, I recently introduced a dedicated resource type for stairs: minecraft_stairs. While stairs are fundamentally just another kind of block in the game, they differ from simpler blocks like stone or dirt because of their directional and shape-specific states. Rather than forcing developers to configure these nuances generically through a monolithic minecraft_block resource, I chose to expose stairs as their own top-level Terraform resource.
This mirrors a well-established pattern in cloud provider Terraform implementations: sometimes a single infrastructure primitive — like a compute instance, container, or storage bucket — can be configured in many different ways. Rather than overloading a generic resource type, providers often split these into multiple dedicated resources that offer targeted schemas, validations, and documentation. I’ve applied the same pattern here to make the experience of working with stairs more intuitive and type-safe.
The Problem with a Generic Block Interface
In Minecraft, stairs are a type of block, but they carry several additional state properties that define how they render and behave in the world. These include:
- The direction the stairs face (facing)
- Whether the stairs are placed on the top or bottom half of the block (half)
- Their shape (straight, inner_left, outer_right, etc.)
- Whether the stairs are waterlogged
These block states go well beyond what’s needed for something like a cobblestone block, which is defined purely by position and material. If I kept all block types under a generic minecraft_block resource, the schema would quickly become overloaded and difficult to validate or document cleanly. Worse, users would be left guessing which properties were valid for which block types.

By creating a separate minecraft_stairs resource, I can expose exactly the set of attributes that are relevant for stairs and validate them appropriately.
HCL Usage: Clean and Intuitive
Here’s an example of how the minecraft_stairs resource looks in HCL:
resource "minecraft_stairs" "altar_top" {
for_each = local.dirs
material = "minecraft:stone_brick_stairs"
position = {
x = -254
y = 69
z = -104
}
facing = "outward"
half = "top"
shape = "straight"
}
This clearly surfaces the intent of the block and gives developers direct access to the specific configuration options that matter for stair blocks.
Go Implementation: Schema Tailored to Use Case
This specialized schema is defined in the Go implementation of the provider using the Terraform Plugin Framework:
func (t stairsResourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
MarkdownDescription: "A Minecraft stairs block (e.g., minecraft:oak_stairs) with orientation and shape.",
Attributes: map[string]tfsdk.Attribute{
"material": {
MarkdownDescription: "The stairs material (e.g., `minecraft:oak_stairs`, `minecraft:stone_brick_stairs`).",
Required: true,
Type: types.StringType,
},
"position": {
MarkdownDescription: "The position of the stairs block.",
Required: true,
Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{
"x": {
MarkdownDescription: "X coordinate of the block",
Type: types.NumberType,
Required: true,
PlanModifiers: tfsdk.AttributePlanModifiers{
tfsdk.RequiresReplace(),
},
},
"y": {
MarkdownDescription: "Y coordinate of the block",
Type: types.NumberType,
Required: true,
PlanModifiers: tfsdk.AttributePlanModifiers{
tfsdk.RequiresReplace(),
},
},
"z": {
MarkdownDescription: "Z coordinate of the block",
Type: types.NumberType,
Required: true,
PlanModifiers: tfsdk.AttributePlanModifiers{
tfsdk.RequiresReplace(),
},
},
}),
},
// Stairs block states
"facing": {
MarkdownDescription: "Direction the stairs face: one of `north`, `south`, `east`, `west`.",
Required: true,
Type: types.StringType,
},
"half": {
MarkdownDescription: "Whether the stairs are on the `top` (upside-down) or `bottom` half.",
Required: true,
Type: types.StringType,
},
"shape": {
MarkdownDescription: "Stair shape: `straight`, `inner_left`, `inner_right`, `outer_left`, or `outer_right`.",
Required: true,
Type: types.StringType,
},
"waterlogged": {
MarkdownDescription: "Whether the stairs are waterlogged.",
Optional: true,
Type: types.BoolType,
},
"id": {
Computed: true,
MarkdownDescription: "ID of the block",
PlanModifiers: tfsdk.AttributePlanModifiers{
tfsdk.UseStateForUnknown(),
},
Type: types.StringType,
},
},
}, nil
}
Connecting HCL to Execution Logic
Once the configuration is parsed, the provider invokes the Minecraft client with the supplied values:
err = client.CreateStairs(
ctx,
data.Material,
data.Position.X, data.Position.Y, data.Position.Z,
// pass through as-is; server expects valid values
data.Facing,
data.Half,
data.Shape,
water,
)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create stairs, got error: %s", err))
return
}
Deletion is handled by simply replacing the stair block with air:
err = client.DeleteBlock(ctx, data.Position.X, data.Position.Y, data.Position.Z)
These low-level actions are abstracted behind a developer-friendly Terraform interface, making the intent and implementation easier to follow and maintain.
func (p *provider) GetResources(ctx context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) {
return map[string]tfsdk.ResourceType{
"minecraft_block": blockResourceType{},
// other types
"minecraft_stairs": stairsResourceType{},
}, nil
}
Design Pattern: Type-Specific Resource Abstractions
This resource design follows a common pattern used across Terraform providers: abstracting a generic backend primitive into multiple high-level resource types that each serve a specific use case.
Each of these resources targets a specific developer experience, with schema definitions and validations tailored to that context.
By introducing minecraft_stairs, I’ve taken a similar approach. While it’s technically possible to treat stairs as a generic block with many attributes, doing so would make the schema less expressive and error-prone. Surfacing a dedicated stair resource unlocks clearer documentation, type-safe configuration, and a better experience for end users writing Terraform HCL.
Conclusion
Stairs are just one example of a block in Minecraft, but their unique configuration needs justify exposing them as a first-class Terraform resource.
By recognizing the distinct nature of stairs and creating the minecraft_stairs resource type, I’ve followed a pattern used by mature cloud providers—splitting abstract resource types into more concrete, context-aware resources.
This approach not only makes the schema more meaningful but also strengthens the link between Terraform HCL and the underlying implementation, ultimately making the provider more intuitive and robust for developers building Minecraft worlds programmatically.
