Blog › ICP guides
OCaml developer on retainer: module system, GADTs, OCaml 5 effects, and Jane Street ecosystem on monthly retainer
September 11, 2026 · ~22 min read
A quantitative trading firm had a latency problem. Their OCaml 4.14 order book processor was recording 8 to 15 millisecond garbage collection pauses during peak trading hours — pauses long enough to miss fills on fast-moving instruments. The minor GC was triggering 250 times per second because the default 256 KB minor heap was filling in 4 milliseconds at the observed allocation rate of 65 MB per second in the market data update path. An OCaml developer on monthly retainer diagnosed the allocation pattern in two sessions: memtrace showed that 94% of allocations originated in the order update parser, which was constructing fresh record values on every incoming message rather than reusing a pre-allocated struct pool.
The fix had two components. The first was a parser redesign: replacing { price = msg.price; qty = msg.qty; side = msg.side } record construction with an object pool using a pre-allocated ring buffer of mutable order records that the parser reused on each update, resetting fields in place rather than allocating. The second was GC parameter tuning: setting OCAMLRUNPARAM=s=4M,b=1 to increase the minor heap to 4 MB, reducing minor collection frequency from 250 per second to 18 per second even during periods when the pool approach did not fully eliminate parser allocations. Maximum GC pause dropped from 15 milliseconds to 1.8 milliseconds, and median order processing latency fell from 8 milliseconds to 0.7 milliseconds without any algorithmic change to the order book data structure.
The retainer invoice logged 19 hours across two sessions: memtrace setup and allocation flamegraph analysis in session one, parser pool implementation and OCAMLRUNPARAM tuning with latency verification under simulated peak load in session two. The 19 hours produced a 11× latency improvement. The change set was 120 lines across four files.
The module system work that retainers fund
OCaml’s module system is the most powerful in any mainstream programming language, and it is the category of retainer work most consistently invisible to a client reviewing a diff. A functor refactoring that eliminates 400 lines of duplicated algorithm code across five concrete implementations appears in the pull request as a deletion of those 400 lines and an addition of 65 lines in a new file. The 16 hours spent designing the module type signature, discovering which helper function signatures had silently diverged across the five implementations, and resolving three compilation errors where a concrete instantiation did not satisfy the functor’s required signature do not appear in the diff at all.
Functors are parameterized modules: they take a module satisfying a module type signature and produce a new module. The canonical use is abstracting over a key type for data structures. A set or map that needs to work for integers, strings, timestamps, and custom domain identifiers would, in a language without functors, require five separate implementations or a runtime type tag and dynamic dispatch. In OCaml, you define the algorithm once against a signature that requires a comparison function, and instantiate it for any concrete type:
module type KEY = sig type t val compare : t -> t -> int val to_string : t -> string end is the module type specifying what any key must provide. module Make (K : KEY) : SET with type elt = K.t = struct ... end is the functor that takes a module satisfying KEY and produces a SET module whose element type is the key type. module IntSet = Make(struct type t = int let compare = Int.compare let to_string = string_of_int end) is one instantiation. module SymbolSet = Make(Symbol) is another, where Symbol is an existing module that already satisfies the KEY signature. The functor body is written once and the compiler verifies at each instantiation site that the argument module satisfies the required signature, eliminating the possibility of providing a key module that has the wrong comparison semantics or a missing helper function.
First-class modules extend this by allowing module implementations to be passed as runtime values: let m = (module MyAlgo : ALGO with type input = int) stores a module value in a variable, and a function that takes (module ALGO with type input = 'a) as an argument can dispatch to different algorithm implementations selected at runtime without giving up the static type checking that makes the dispatch safe. This pattern appears frequently in plugin architectures and configurable backends where the concrete implementation is determined by a configuration file or command-line flag parsed at startup.
GADT type-safe API design
Generalized algebraic data types (GADTs) allow each constructor of a variant type to specify a different return type for the type parameter, enabling the type of an expression to depend on which constructor was used. The standard illustration is a typed expression language:
type _ expr = Int : int -> int expr | Bool : bool -> bool expr | Add : int expr * int expr -> int expr | If : bool expr * ‘a expr * ‘a expr -> ‘a expr defines an expression type where int expr represents an expression that evaluates to an integer and bool expr represents one that evaluates to a boolean. The eval function has type ‘a expr -> ‘a: it takes an expression whose type parameter encodes the result type and returns a value of that type. The If constructor enforces at the type level that both branches have the same type and that the condition is a boolean expression. None of these constraints require runtime checks; the compiler verifies them at the point where the expression tree is constructed.
In practice, GADT-based retainer work typically involves converting a system that uses runtime type tags and match-on-tag dispatch into one where the type tag is encoded in the GADT index, eliminating a class of runtime type mismatch errors. A configuration system with a value type that uses variant constructors like Int_val of int | Str_val of string | Bool_val of bool and a get : string -> value lookup function requires the caller to match on the constructor, handle the wrong-constructor case, and trust that the registry was populated with the correct type. The GADT encoding uses a typed key: type ‘a key = Int_key : string -> int key | Str_key : string -> string key | Bool_key : string -> bool key so that get : ‘a key -> ‘a returns a concretely typed value determined by the key type, and the registry’s implementation uses an existential type to store heterogeneous values without exposing the implementation type to the caller.
Phantom types are a lighter-weight alternative for state machine encoding. type ‘state connection constraint ‘state = [< `connected | `disconnected ] with separate constructors for connected and disconnected connections allows functions that require an active connection (like send : ‘connected connection -> bytes -> unit) to be statically prevented from being called on a disconnected connection, eliminating the runtime check and the associated error handling code. A retainer engagement covering phantom type migration typically involves identifying the places in the codebase where connection state is checked with an if-statement or Option.is_some guard, encoding the state as a phantom type parameter, and threading the type parameter through the data structures that carry the connection value so that the compiler tracks state at every call site.
OCaml 5 multicore domains and algebraic effects
OCaml 5 introduces two major features: Domain for true multicore parallelism (each domain runs on a separate processor core with its own minor heap) and algebraic effects for structured non-blocking concurrency without the function coloring problem that async/await introduces in languages like JavaScript, Python, and Rust.
Domain adoption in OCaml 5 retainer engagements typically involves three phases. The first is auditing the existing codebase for thread-unsafe mutable state: global ref values and mutable record fields that are written from the main domain and would be subject to data races if multiple domains run concurrently. OCaml 5’s memory model provides weaker guarantees than Java’s happens-before model; accesses to mutable values across domain boundaries require explicit synchronization using Atomic for single-word values or Mutex for multi-field updates. The second phase is restructuring the computation into domain-parallel units: partitioning the work into chunks that can be computed independently (let domains = Array.init n_cores (fun i -> Domain.spawn (fun () -> compute_chunk chunks.(i)))), joining the results (Array.map Domain.join domains), and verifying that the partition strategy eliminates false sharing (adjacent array elements accessed by different domains that fall in the same cache line). The third phase is verifying correctness under the OCaml 5 memory model using the ThreadSanitizer integration available via ocamlfind ocamlopt -package tsan and running the parallel test suite under TSan to detect race conditions that the type system does not rule out.
Algebraic effects address the function coloring problem differently from Rust’s async/await or Haskell’s IO monad. In OCaml 5, an effect is declared with type _ Effect.t += Read : int -> bytes Effect.t and performed with let data = Effect.perform (Read fd) in a function that does not need to be marked as async or return a deferred type. The effect is handled by a surrounding Effect.Deep.match_with handler that decides how to implement the effect: a synchronous handler for testing can return a fixed byte sequence; a production handler can register the file descriptor with epoll and resume the continuation when data is available without blocking the executing domain. The retainer work involved in adopting effects is typically: defining the effect type signatures for the I/O operations the codebase performs, writing handlers for both synchronous (testing) and asynchronous (epoll/io_uring) implementations, threading the handler through the call stack so that performs in library code are captured by the application-level handler, and verifying that deep continuations (effects performed inside other handlers) are handled correctly by the outer handler chain.
Jane Street ecosystem: Core, Async, and ppx_jane
Jane Street’s open-source OCaml ecosystem provides production-quality replacements for OCaml’s standard library (Base and Core), a concurrent programming library (Async), and a suite of ppx rewriters that reduce boilerplate for serialization, comparison, field access, and variant handling (ppx_jane). Adopting this ecosystem in a retainer engagement is typically a phased migration: replacing List.map with Base.List.map, replacing Hashtbl.find with Base.Hashtbl.find (which returns an Option rather than raising Not_found), and replacing Printf.sprintf with Core.sprintf — each change is small but the aggregate effect is a codebase where Not_found exceptions cannot be raised from lookups and Invalid_argument exceptions from negative list indices are replaced by explicit Option returns.
Async’s deferred programming model wraps every value that may not yet be available in a Deferred.t type and composes deferred computations using let%bind result = Async.Reader.read_line reader in (the ppx_let syntax for Deferred.bind). A retainer engagement covering Async migration typically involves: identifying the blocking I/O calls in the codebase (Unix.read, In_channel.input_line, socket operations), replacing each with the Async equivalent (Async.Reader.read, Async.Reader.read_line, Async_unix.Tcp.connect), converting the functions that call them to return Deferred.t and thread the deferred through the call chain with let%bind, and writing the top-level scheduler invocation (Async.Scheduler.go () or the Command.async entry point for command-line programs).
ppx_jane ppx rewriters are the most visible productivity improvement from the Jane Street ecosystem. Adding [@@deriving sexp] to a type definition generates sexp_of_t : t -> Sexp.t and t_of_sexp : Sexp.t -> t functions that serialize and deserialize the value to S-expression format, used throughout Jane Street’s debugging infrastructure and configuration files. Adding [@@deriving compare, equal] generates structural comparison and equality functions that do not rely on OCaml’s polymorphic comparison (which is correct but unoptimized for custom types). Adding [@@deriving fields] generates Fields.iter : ?field1:('a -> unit) -> ... -> t -> unit for reflection over record fields by name, used in form validation, ORM mapping, and configuration serialization. A retainer engagement covering ppx_jane adoption typically involves adding the deriving attributes to the core domain types, verifying that the generated sexp serialization round-trips correctly through a property test, and updating the dune build configuration to include (preprocess (pps ppx_jane)) in the affected library stanzas.
How HourTab tracks OCaml developer retainer hours
OCaml developer retainers present a specific hour-visibility problem: a 16-hour functor refactoring session produces a net deletion of 340 lines and an addition of 65 lines. The diff is visible and its size undersells the work: the 340 lines deleted were five diverged copies of the same algorithm, and the 65 lines added are a functor body plus five three-line instantiations. The 16 hours spent discovering the divergence, designing the module type signature, and resolving the three instantiation type errors that surfaced incompatible helper function signatures do not appear anywhere in the git log unless the commit message documents them explicitly.
HourTab gives OCaml consultants a public retainer-hours URL that shows the client the burn-down without a status email. The work log entry is where the module design rationale is documented: the functor signature chosen and alternatives considered, the diverged helper signatures discovered, the GADT encoding that replaced the runtime type tag, the memtrace allocation site identified, the OCAMLRUNPARAM settings tested and rejected before the final tuning was accepted. That entry takes five minutes to write after the session and converts a 16-hour black box into a 16-hour analysis with documented findings. The next client check-in is a two-sentence summary (“functor refactoring eliminated 340 lines of duplicated order book logic; the diverged to_protobuf helper in the Symbol implementation was the only bug found in the process”) rather than a thirty-minute tutorial on OCaml’s module system.
The retainer model fits OCaml development because the architectural work is continuous and non-uniform. An upgrade from OCaml 4.14 to OCaml 5.1 requires auditing every global mutable value for domain-safety and redesigning the concurrent sections of the codebase for the new memory model. A new performance requirement requires memtrace profiling, allocation audit, GC tuning, and latency verification under realistic load. A new data structure requirement requires deciding whether a functor, a GADT, or a first-class module is the right abstraction and designing the module type signature before writing any implementation. A project contract closes when the feature ships. A retainer stays open for the OCaml 5 upgrade, the next GC tuning cycle, and the next functor signature that needs to be designed before the implementation team writes five diverging copies of the same algorithm.
Track OCaml developer retainer hours without the status emails
HourTab gives OCaml consultants and functional programming engineers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your session work log becomes the proof of module system advisory that gets the retainer renewed.
See HourTab pricing →FAQ: OCaml developer retainers
What does an OCaml developer on retainer typically do?
An OCaml developer or functional programming consultant on monthly retainer provides ongoing module system architecture (functor design, module type signatures, first-class modules), GADT and phantom type encoding for compile-time correctness guarantees, OCaml 5 multicore Domain parallelism and algebraic effect handler design, Jane Street Core/Async/ppx_jane ecosystem integration, and production GC tuning with memtrace allocation profiling and OCAMLRUNPARAM optimization. The retainer covers the type system and performance engineering between visible feature releases: functor refactorings that eliminate duplicated algorithm code, GADT encodings that replace runtime type tags with compile-time checks, Domain migrations that add multicore parallelism, and GC tunings that reduce latency spikes in production.
What OCaml work is most underlogged in a retainer?
Functor refactoring (discovering diverged algorithm copies across concrete implementations, designing the module type signature, resolving instantiation type errors), GC tuning for latency-sensitive applications (memtrace allocation profiling, OCAMLRUNPARAM minor heap sizing and space_overhead adjustment, allocation pool design for hot-path parsers), and GADT type-safe API design (converting runtime type tag systems to compile-time GADT index encoding, eliminating wrong-type-at-call-site runtime errors) are the three most systematically underlogged OCaml retainer categories. Each produces a small diff with a large correctness or performance improvement whose analysis hours are entirely invisible in the git log.
What are typical OCaml developer retainer rates?
Entry-level OCaml developers (1–3 years, basic module system, Lwt, dune, pattern matching) bill at $100–$175/hr. Mid-level OCaml engineers (3–8 years, functor design, phantom types, Jane Street Base/Core/Async, ppx_jane, memtrace profiling, GC parameter tuning) bill at $160–$295/hr. Senior OCaml architects (8+ years, GADTs, first-class modules, OCaml 5 Domain + algebraic effects, ppxlib custom ppx rewriter authoring, Bin_prot binary protocol design, Flambda inliner, formal verification integration) bill at $225–$415/hr. Firm rates run $185–$335/hr. Monthly retainer ranges: $5,000–$9,000/mo for advisory (15–30 hrs), $12,000–$24,000/mo for full-engagement (module system redesign plus performance plus multicore adoption).
What should an OCaml developer retainer agreement include?
An OCaml developer retainer agreement should specify scope boundary between module system architecture (functor design, first-class modules, recursive modules), type system advisory (GADTs, phantom types, polymorphic variants, existential types), OCaml 5 multicore adoption (Domain parallelism, algebraic effects, Atomic and Mutex synchronization, TSan race detection), Jane Street ecosystem work (Base/Core migration, Async deferred programming, ppx_jane ppx rewriter adoption, Bin_prot binary serialization), and performance and GC advisory (memtrace allocation profiling, OCAMLRUNPARAM tuning, Flambda inliner configuration). Include OCaml version scope (4.14.x, 5.0.x, 5.1+), Jane Street ecosystem scope (Base/Core only vs. Async vs. Incremental vs. ppxlib custom rewriters), and hour logging specifics (functor signature designed, GADT encoding chosen, memtrace flamegraph site identified, OCAMLRUNPARAM values tested and accepted). Monthly retainer: $5,000–$9,000/mo advisory, $12,000–$24,000/mo full engagement.
How should OCaml developer retainer hours be logged?
Log each OCaml retainer session with: advisory category (module type signature design, functor implementation, functor instantiation, first-class module, module inclusion/open, recursive module, phantom type constraint, GADT type index, GADT eval function, polymorphic variant, existential type, OCaml 5 Domain.spawn/join, Atomic CAS, Mutex/Condition, Effect.perform/continue, effect handler match_with, Domain.DLS, Lwt bind/catch, Async Deferred.bind/let%bind, ppx_sexp_conv, ppx_compare, ppx_fields, ppx_variants, ppx_jane deriving, Bin_prot binary serialization, Core.Hashtbl/Map/Set, Base.List/String/Option, memtrace flamegraph, OCAMLRUNPARAM s/b/o tuning, Flambda @inline, dune library/virtual/ppx, alcotest/qcheck), the specific module or function, the problem identified (type error message, allocation flamegraph percentage, GC pause milliseconds, line count of duplicated code), the solution applied (functor signature, GADT constructor, effect definition, OCAMLRUNPARAM value), and before/after metrics. Use dollar-sign format: [category] — [module] — [task] — [problem, work, resolution] — [before/after] — [hours].