The graveyard of every infrastructure codebase is full of modules that someone wrote once, used once, and never touched again. The code that actually gets reused shares a few patterns worth naming.

Make the interface obvious

Variables are your module's API. If someone has to read your main.tf to understand what the module does, the interface has failed. Every variable should have a description. Required variables should be few.

variable "environment" {
  description = "Deployment environment: dev, staging, or prod"
  type        = string
  validation {
    condition     = contains(["dev","staging","prod"], var.environment)
    error_message = "Must be dev, staging, or prod."
  }
}

Outputs as contracts

Every module should output everything a caller might need. If you're building a VPC module, output the VPC ID, all subnet IDs, the default security group, and the NAT gateway IPs.

Version your modules

Pin module versions in callers. Use Git tags. This is the single change that prevented the most incidents on my team.

module "vpc" {
  source = "git::https://github.com/sujanmagar/tf-modules//vpc"
  ref    = "v1.4.0"   # pinned — never use main/master
}