Composing Programs

Real tasks are pipelines, not single calls, and in DSPy you build them the way you build a neural network — subclass a base module, declare sub-modules, and wire them together in a forward method.

Single modules handle single transformations. Real systems chain several — rewrite a query, retrieve, reason, generate, verify. DSPy composes these into a program: a dspy.Module subclass that holds sub-modules and connects them in a forward method. If you have used PyTorch, the shape is deliberately familiar. This fourth post in the DSPy series covers composing programs, why the structure matters for optimization, and a worked RAG example.

A program is a module made of modules

DSPy programs are built exactly like the modules they contain — by subclassing dspy.Module, declaring sub-modules in __init__, and defining the data flow in forward:

import dspy

class MultiHop(dspy.Module):
    def __init__(self):
        self.generate_query = dspy.ChainOfThought("question -> search_query")
        self.generate_answer = dspy.ChainOfThought("context, question -> answer")

    def forward(self, question):
        query = self.generate_query(question=question).search_query
        context = search(query)                     # your retriever
        return self.generate_answer(context=context, question=question)

Two sub-modules, each with its own signature and strategy, wired together by ordinary Python in forward. The control flow is just code — loops, conditionals, function calls — so a program can be as simple or as elaborate as the task needs. This is the same compositional pattern as the ReAct module you saw earlier; an agent is one kind of program, and you can write your own.

Why the structure matters: optimization sees inside

The reason to build programs this way — rather than concatenating everything into one giant prompt — is that DSPy’s optimizer can see and tune each sub-module independently. Because generate_query and generate_answer are declared as distinct parameterized modules, the optimizer can find the best instruction and demonstrations for the query-rewriting step and, separately, for the answering step. A monolithic prompt offers one blob to tune; a structured program offers several tunable components, each optimized for its own role. The structure you write is the structure the optimizer improves.

This is the payoff of the “programming, not prompting” stance at pipeline scale. You decompose the task into declared steps; DSPy compiles each step’s prompt; and the whole pipeline improves against a single end-to-end metric even though its parts are tuned individually.

A RAG program

Retrieval-augmented generation is the canonical multi-step program, and DSPy expresses it cleanly. A minimal RAG module retrieves context for the question and generates a grounded answer:

class RAG(dspy.Module):
    def __init__(self, retriever):
        self.retriever = retriever
        self.respond = dspy.ChainOfThought("context, question -> answer")

    def forward(self, question):
        context = self.retriever(question)          # returns passages
        return self.respond(context=context, question=question)

rag = RAG(my_retriever)
rag(question="What is our refund window?").answer

The retriever is ordinary code — any vector search, hybrid search, or API you like — and the generation step is a declared module. Because respond is parameterized, optimizing this program will tune how the model uses the retrieved context, improving faithfulness and answer quality against your metric without you hand-writing the RAG prompt. If you add a query-rewriting step or a reranking step, they become additional sub-modules the optimizer also tunes — the pipeline grows in structure, not in prompt-string complexity.

Composition patterns

The dspy.Module + forward pattern accommodates the common pipeline shapes:

Because it is all Python, you are not constrained by a diagram language — you express the flow directly. And because every LLM step is a declared module, every one of them remains tunable no matter how deep the composition.

Keep programs decomposed for a reason

There is a temptation, once you know how, to collapse a pipeline back into one clever module with a huge signature. Resist it for the same reason you decompose functions in ordinary code, plus one DSPy-specific reason: a step that is folded into another cannot be optimized on its own. Keeping distinct transformations as distinct modules gives the optimizer more, smaller, better-defined targets — and each is easier to test and reason about. The right granularity is one module per genuine transformation: query rewriting, retrieval reasoning, answering, verification each earn their own module; trivial glue stays as plain code in forward.

Key takeaways

Further reading

Sources & References

Building programs