How do I implement the builder pattern in EK9?

← Classes and OOP · Ref: Q118

EK9 supports builder-like patterns through record merge operations and multiple constructors. The merge operator ':~:' is particularly useful for incremental object construction.

BUILDER IN EK9

Use a record with the merge operator to build up an object:

  config <- ServerConfig()
  config :~: partial1
  config :~: partial2

Each merge adds only the SET fields from the source.

FLUENT API PATTERN

Define builder methods that return the modified object:

  withHost()
    -> host as String
    <- rtn as ServerConfig: ServerConfig()
    rtn :=: this
    rtn.host: host

USING RECORDS AS BUILDERS

Records with public fields and merge operator serve as natural builders. Create partial records and merge them together.

ALTERNATIVE NAMED CONSTRUCTOR PARAMETERS

EK9 supports named parameters in constructors:

  config <- ServerConfig(host: "localhost", port: 8080)

This can replace simple builders.

See Q94 for constructors. See Q97 for records. See Q98 for record operators.

Example

defines module qa.oop.builder

  defines record

    ServerConfig
      host <- String()
      port <- Integer()
      maxConnections <- Integer()

      ServerConfig()
        ->
          host as String
          port as Integer
          maxConnections as Integer
        this.host: host
        this.port: port
        this.maxConnections: maxConnections

      operator :=:
        -> from as ServerConfig
        host :=: from.host
        port :=: from.port
        maxConnections :=: from.maxConnections

      operator :~:
        -> from as ServerConfig
        if from.host?
          host :=: from.host
        if from.port?
          port :=: from.port
        if from.maxConnections?
          maxConnections :=: from.maxConnections

      operator $ as pure
        <- rtn as String: `${host}:${port} (max:${maxConnections})`

      default operator ?

  defines program

    BuilderDemo()
      stdout <- Stdout()

      // === NAMED CONSTRUCTOR PARAMETERS ===

      config1 <- ServerConfig("localhost", 8080, 100)
      stdout.println(`Direct: ${config1}`)

      // === BUILD WITH MERGE ===

      config2 <- ServerConfig()
      partial1 <- ServerConfig()
      partial1.host: "production.example.com"
      config2 :~: partial1

      partial2 <- ServerConfig()
      partial2.port: 443
      partial2.maxConnections: 500
      config2 :~: partial2

      stdout.println(`Merged: ${config2}`)

      // === COPY AND MODIFY ===

      config3 <- ServerConfig()
      config3 :=: config1
      config3.port: 9090
      stdout.println(`Modified copy: ${config3}`)

Common mistakes

E06180 — Class fields are private and cannot be accessed externally. Changing the record to a class makes 'partial1.host: ...' trigger E06180 — not accessible from this context. Use records for builder patterns because record fields are public. See ek9 -h E06180 for details.

Incorrect:

defines class

Correct:

defines record

E50060 — The replace operator ':^:' is not defined on ServerConfig. Using ':^:' instead of ':~:' triggers E50060 — method/function not resolved. Define the operator or use the correct one. See ek9 -h E50060 for details.

Incorrect:

config2 :^: partial2

Correct:

config2 :~: partial2

E50060 — The replace operator ':^:' is not defined on ServerConfig. Using ':^:' instead of ':=:' triggers E50060 — method/function not resolved. Define the operator or use the correct one. See ek9 -h E50060 for details.

Incorrect:

config3 :^: config1

Correct:

config3 :=: config1

E50001 — EK9 does not have a Java-style builder with method chaining. Use constructors for direct creation or the merge operator ':~:' for incremental building. See ek9 -h E50001 for details.

Incorrect:

config1 <- ServerConfig.builder().host("localhost").port(8080).build()

Correct:

config1 <- ServerConfig("localhost", 8080, 100)

E07430 — Mutation operators (:~:, :=:, :^:, +=, -=) mutate the object in place and must not have return values. Adding a return declaration triggers E07430. These operators modify 'this' directly. See ek9 -h E07430 for details.

Incorrect:

operator :~:
        -> from as ServerConfig
        <- rtn as ServerConfig: ServerConfig()

Correct:

operator :~:
        -> from as ServerConfig

E50060 — EK9 does not have a toString() method. Use the $ operator or string interpolation. The record's operator $ handles string conversion. See ek9 -h E50060 for details.

Incorrect:

stdout.println(config1.toString())

Correct:

stdout.println(`Direct: ${config1}`)
Other ways to ask this
  • How do I build complex objects step by step in EK9?
  • Does EK9 support fluent builder APIs?
  • How do I use record merge for building objects?

Coming from another language?

Java: Builder pattern with nested static class, method chaining, .build() call. Lombok @Builder for auto-generation. Python: kwargs in constructors, or dataclass with defaults. Rust: builder pattern crate or struct update syntax with '..' operator. Go: functional options pattern with variadic args. Kotlin: named parameters and default values replace most builders, apply{} for mutation. EK9: record merge operator ':~:' for incremental construction, named constructor parameters, multiple constructor overloads.

Keywords: builder, construct, incremental, named, merge, object-oriented, fluent, compile, parameter, record, pattern, step