Overview
Prototype is a creational design pattern used to clone baseline objects safely when creation cost or setup is high. In real backend services, this pattern helps teams keep modules decoupled while still shipping features quickly.
Problem It Solves
Without Prototype, teams often mix orchestration, external integration, and domain rules in one place. That leads to high coupling, difficult testing, and expensive refactoring.
5W + 1H
What
Prototype is a pattern that structures collaboration between components to solve a recurring design problem.
Why
To increase maintainability, isolate change, and enforce clear boundaries between policy and implementation detail.
Who
Backend engineers, platform engineers, and API teams working with integrations and evolving business logic.
When
Use Prototype when your service complexity is growing and one responsibility starts bleeding into unrelated modules.
Where
Apply it in service boundaries such as application use cases, integration adapters, workflow orchestration, and domain policies.
How
Define stable ports first, then implement adapters and wire them through use-case orchestration.
Go Implementation (Step-by-step)
1) Port / interface definitions
package ports
import "context"
type PrototypeInput struct {
RequestID string
Payload map[string]any
}
type PrototypeOutput struct {
Status string
Data map[string]any
}
type PrototypePort interface {
Execute(ctx context.Context, in PrototypeInput) (PrototypeOutput, error)
}2) Adapter / implementation
package adapters
import (
"context"
"fmt"
"myapp/internal/ports"
)
type PrototypeAdapter struct {
providerName string
}
func NewPrototypeAdapter(providerName string) *PrototypeAdapter {
return &PrototypeAdapter{providerName: providerName}
}
func (a *PrototypeAdapter) Execute(ctx context.Context, in ports.PrototypeInput) (ports.PrototypeOutput, error) {
if in.RequestID == "" {
return ports.PrototypeOutput{}, fmt.Errorf("request id is required")
}
return ports.PrototypeOutput{
Status: "ok",
Data: map[string]any{
"pattern": "Prototype",
"provider": a.providerName,
},
}, nil
}3) Use-case / main wiring
package main
import (
"context"
"fmt"
"log"
"myapp/internal/adapters"
"myapp/internal/ports"
)
func run(ctx context.Context, svc ports.PrototypePort) error {
out, err := svc.Execute(ctx, ports.PrototypeInput{
RequestID: "REQ-1001",
Payload: map[string]any{"source": "api"},
})
if err != nil {
return err
}
fmt.Println("status:", out.Status, "pattern:", out.Data["pattern"])
return nil
}
func main() {
ctx := context.Background()
service := adapters.NewPrototypeAdapter("default-provider")
if err := run(ctx, service); err != nil {
log.Fatal(err)
}
}Suggested Project Structure
cmd/
api/
main.go
internal/
domain/
prototype-pattern-go/
model.go
policy.go
application/
prototype-pattern-go/
usecase.go
ports/
prototype-pattern-go_port.go
adapters/
inbound/
http/
prototype-pattern-go_handler.go
outbound/
prototype-pattern-go_adapter.go
infrastructure/
config/
loader.go
persistence/
repository.goClean/Hexagonal Placement
- Domain: keeps pure business rules and entities related to Prototype usage.
- Application (Use Case): orchestrates request flow and coordinates domain + ports.
- Ports: defines stable contracts (
PrototypePort) that core logic depends on. - Adapters: implements port behavior (HTTP, gRPC, vendor API, queue consumer).
- Infrastructure: framework-specific and provider-specific setup.
Boundary Rules
- Dependencies point inward: adapters/infrastructure depend on ports/domain, not the reverse.
- Domain must stay framework-agnostic (no HTTP, DB driver, or vendor SDK import).
- Application layer should know interfaces, not concrete adapters.
- Infrastructure may know everything technical, but should not hold business policy.
Real-World Use Case
In production, Prototype is useful to clone campaign rule templates for many tenants. You can keep your use case stable while replacing providers or transport implementations with minimal changes.
Benefits & Tradeoffs
Benefits
- Better modularity and maintainability.
- Easier testability with mocked ports.
- Safer refactoring because boundaries are explicit.
Tradeoffs
- More files and abstractions to maintain.
- Initial learning curve for team members unfamiliar with layered architecture.
Common Pitfalls
- Putting domain rules in adapters.
- Leaking vendor-specific payload directly into domain models.
- Creating too many abstractions without concrete change pressure.
- Skipping contract tests for port behavior.
When NOT to use
- Service is still very small and not expected to grow.
- Team needs a quick prototype and architecture overhead would block delivery.
- There is only one stable integration with no foreseeable variation.
Conclusion
Prototype is most effective when paired with Clean/Hexagonal boundaries. The pattern gives structure, while architecture keeps dependencies under control as the codebase evolves.