Skip to main content
  1. Temporal Internals/

Chapter 8: Application Composition and Lifecycle #

In Chapter 7: Cluster Membership and Internal RPC, we saw how many Temporal nodes work together as a cluster. Now let’s zoom out one level further and ask a bigger question: How is the whole Temporal server application put together?

The server is not one giant main() function. It is built from many pieces: frontend, history, matching, persistence, visibility, and more. Someone has to wire these pieces together in the right order. Someone also has to start them and stop them without causing errors.

That “someone” is go.uber.org/fx.

You can think of fx as the electrical panel of a building. The building has many circuits: lights, outlets, AC, elevators. All of them need to be connected to the correct breakers, then switched on in the right order. If you turn on the elevator before the electricity is stable, you might have problems. fx handles that wiring for Temporal.


The Problem: Many Modules, One Application #

Imagine if you had to write the Temporal server by hand:

p := persistence.New()
h := history.New(p)
m := matching.New(p)
v := visibility.New(p)
f := frontend.New(h, m, v)
f.Start()

That is not too bad for five modules. But the real server has many more dependencies, configuration options, and lifecycle concerns. What if one module needs a database connection before another starts? What if a service fails to start? What if you need to stop everything gracefully?

You need a framework that:

  • Knows what each module needs.
  • Builds modules in dependency order.
  • Starts them in the correct order.
  • Stops them in the reverse order when the server shuts down.

That is exactly what fx does.


A Concrete Use Case: Start the Whole Temporal Server #

Let’s say you have cloned the Temporal server repository and you want to run it locally. The simplified command is:

go run ./cmd/server start

As the server starts, fx logs the modules it is creating. The exact output will be more detailed, but imagine seeing something like this:

[Fx] PROVIDE    *persistence.Persistence
[Fx] PROVIDE    *history.Service
[Fx] PROVIDE    *matching.Service
[Fx] PROVIDE    *visibility.Service
[Fx] PROVIDE    *frontend.Service
[Fx] PROVIDE    server.Start
[Fx] START      *persistence.Persistence
[Fx] START      *history.Service
[Fx] START      *matching.Service
[Fx] START      *visibility.Service
[Fx] START      *frontend.Service
Temporal server listening on 127.0.0.1:7233

The important part is not the exact log text. The important part is the order. Persistence starts first, then history, then matching, then visibility, and finally the frontend. When the server stops, the order is reversed: frontend stops first, then internal services, then persistence.


Key Concepts #

Let’s break down the main ideas.

1. Application Composition #

Composition means taking separate pieces and combining them into one complete application.

Temporal has logical modules:

ModuleWhat it doesWhere we saw it
frontendReceives public gRPC API requestsChapter 1: Public API and Service Routing
historyStores workflow state and historyChapter 2: Durable Persistence
matchingDelivers tasks to workersChapter 3: Task Queues and Matching
visibilitySearches workflows by attributesChapter 4: Visibility and Search
persistenceProvides database accessUnder all of the above

Each module has dependencies. For example:

  • history needs persistence.
  • matching needs persistence.
  • visibility needs persistence and a search backend.
  • frontend needs history, matching, and visibility.

fx figures out this dependency graph for us.

2. Dependency Injection #

Dependency injection means that a module does not create its own dependencies. Instead, it says:

“I need a persistence.Persistence object. Please give it to me.”

Then fx provides it.

In Go, this is expressed with function parameters:

func history.New(p *persistence.Persistence) *history.Service

This says: history.New needs a *persistence.Persistence, and returns a *history.Service.

Similarly:

func frontend.New(h *history.Service, m *matching.Service, v *visibility.Service) *frontend.Service

fx looks at these function signatures and says: “To build a frontend, I first need to build history, matching, and visibility. And before that, I need persistence.”

This is why we don’t need to write manual wiring code like h := history.New(p).

3. The Object Graph #

The dependency relationships form a graph.

Think of it like a family tree:

  • frontend depends on history
  • history depends on persistence
  • so persistence must be created before history
  • and history must be created before frontend

fx builds this graph automatically. If you register a constructor with fx.Provide, it becomes a node in the graph. If an object is never needed, fx may not even create it.

4. Lifecycle Hooks #

A module doesn’t just exist. It has to be started and stopped.

For example:

  • Before the frontend can accept requests, it needs to open a network port.
  • Before the history service can be used, it needs database connections.
  • When the server stops, connections should be closed gracefully.

fx lets each module register OnStart and OnStop hooks.

Here’s a simplified example:

func NewFrontend(lc fx.Lifecycle, h *history.Service) *frontend.Service {
    s := frontend.New(h)
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error { return s.Start() },
        OnStop:  func(ctx context.Context) error { return s.Stop() },
    })
    return s
}

fx.Lifecycle is automatically provided by fx. When the application starts, fx calls OnStart. When it stops, fx calls OnStop.

Important: the start order comes from dependencies. If frontend needs history, then history starts first. Stopping happens in the reverse order: frontend stops first, then history.


How FX Solves the Use Case #

Now let’s write a simplified main function that uses fx.

app := fx.New(
    fx.Provide(persistence.New),
    fx.Provide(history.New),
    fx.Provide(matching.New),
    fx.Provide(visibility.New),
    fx.Provide(frontend.New),
    fx.Invoke(server.Start),
)
app.Run()

Let’s walk through it line by line.

  • fx.New(...) creates a new application container.
  • fx.Provide(...) tells fx: “Here is a constructor that knows how to build an object.”
  • fx.Invoke(server.Start) tells fx: “When the app starts, call this function.”
  • app.Run() starts the app and waits for a stop signal.

The server.Start function may look like this:

func Start(srv *frontend.Service) {
    srv.ListenAndServe()
}

This function needs a *frontend.Service. Because fx sees that dependency, it builds the whole graph needed for the frontend.

No one manually wrote:

h := history.New(p)
m := matching.New(p)
f := frontend.New(h, m)

fx did that automatically.


What Happens Under the Hood? #

Let’s trace the lifecycle of a server start with a simple sequence diagram.

sequenceDiagram participant M as Main participant F as FX participant P as Persistence participant H as History Service participant S as Frontend Service M->>F: app.Run() F->>P: Construct persistence F->>H: Construct history (uses persistence) F->>S: Construct frontend (uses history) F->>P: OnStart persistence F->>H: OnStart history F->>S: OnStart frontend S-->>M: Listening on :7233

Step by step:

  1. app.Run() is called.
  2. fx looks at the server.Start function and sees it needs a frontend.Service.
  3. To build the frontend, fx sees the frontend needs a history service.
  4. To build history, fx sees history needs persistence.
  5. fx builds persistence first, then history, then frontend.
  6. After all objects are constructed, fx calls OnStart hooks in dependency order.
  7. Once every OnStart succeeds, the server is ready.

If any OnStart fails, fx stops the hooks that already started. This is like a circuit breaker: if one circuit causes a problem, the panel safely turns off the circuits that were already powered on.


Diving Into the Code #

The fx library appears in the project’s go.mod file:

// go.mod (excerpt)
go.uber.org/fx v1.24.0
go.uber.org/dig v1.19.0 // indirect

fx uses go.uber.org/dig underneath to manage the dependency graph. dig is the low-level container; fx adds lifecycle management on top.

In the Temporal server repository, the real entry point lives in cmd/server/main.go. The real code is more complex because it handles configuration, logging, feature flags, and multiple server roles. But conceptually, it uses the same pattern:

app := fx.New(
    fx.Provide(persistence.New),
    fx.Provide(history.New),
    fx.Provide(matching.New),
    fx.Provide(visibility.New),
    fx.Provide(frontend.New),
    fx.Invoke(server.Start),
)

Each module registers lifecycle hooks in its own package. For example, the history service might start its event loading loop in OnStart, and the frontend service starts accepting gRPC requests in its own OnStart.

The key is that no module tells another module when to start. Each module only declares what it needs. fx decides the order.


Why This Feels Like an Electrical Panel #

Let’s go back to the building analogy.

  • The modules are the circuits.
  • The constructors are the wiring instructions.
  • The lifecycle hooks are the breakers.
  • fx is the panel that connects everything and switches it on in the correct order.
  • app.Run() is the main switch for the whole building.

If you simply wrote frontend.Start() without starting persistence first, the frontend might try to use a database that is not ready. With fx, that can’t happen, because the dependency graph forces the correct order.


Conclusion #

Application composition is about building a complex system from smaller parts. Lifecycle management is about starting and stopping those parts safely. Temporal uses go.uber.org/fx to do both.

You learned:

  • The Temporal server is a composition of frontend, history, matching, persistence, and visibility modules.
  • fx.Provide registers constructors.
  • fx.Invoke triggers the construction of objects needed to start the app.
  • Lifecycle hooks run OnStart and OnStop in dependency order.
  • Startup order follows dependencies; shutdown order is the reverse.

This is the final chapter of the tutorial. Now you have the full picture: from the public API to persistence, from task queues to visibility, from observability to cluster membership, and finally to how the whole server is wired together. If you go back to Chapter 1: Public API and Service Routing, the whole journey should feel even clearer.


Generated by AI Codebase Knowledge Builder