Noteflakes iconnoteflakes

Tailor-made software

My name is Sharon and I build custom software solutions for my clients.

Noteflakes is my independent software company based in France. My main fields of expertise are:

  • Internet-enabled process-control systems.
  • Integration of internet services for industrial and B2B apps.
  • Storage, retrieval and analysis of time series data (for industrial and B2B applications).

I build custom solutions for my clients, based on my many years of experience in integrating process-control systems with internet platforms in a secure and robust manner. Please feel free to contact me, I’d love to hear about your project!


Recently on noteflakes:

18·06·2026

Rethinking modularity in Ruby applications

I wrote recently about Syntropy, a new Ruby web framework I’m working on (it runs this site). Syntropy’s design is based around the idea of file-based routing, which means that the source files for route handlers (i.e. controllers) that make up the app are organized and named according to the app’s URL namespace. I also discussed the way Syntropy loads the different source files (referred to as modules), and I’d like to discuss this a bit more in detail.

Code Organization in Ruby on Rails

Now, if you’re a Rails developer, you know that Rails’ approach to code organization is based on auto-loading of the different source files that make up the app, performed by the Zeitwerk gem. This approach automates the loading of dependencies, using the directory structure as a representation of the app’s namespace (i.e. classes and modules).

In the Rails approach, all of the app’s classes and modules are global, nested according to the app’s directory structure. There’s no need to explicitly require dependencies, since any constant reference will be automatically loaded, and this means the dependencies between different parts of app are implicit.

This approach works very well, as long as you’re organizing your code in classes that follow certain conventions in terms of naming and file locations. A big advantage is that you don’t need to explicitly require the different source files in your app. Instead, they’re loaded automatically as constants are referenced by running code. Zeitwerk even supports automatic reloading of changed files in development mode.

One important disadvantage of the Rails approach is that you’re bound to the idea of one class per file, and of course the names for the different classes must match the source file’s location. This means that if you move a source file to a different directory, you’ll also need to rename the class to match its new location.

A further disadvantage is that since everything is global, you might risk touching or referencing classes you shouldn’t, and since dependencies are implicit, those “references by error” may go unnoticed and cause some unexpected (read: undefined) behaviour. For example, since any singleton object (and singleton objects are quite handy in web apps) is defined as a global, your code might accidentally access that global object, or even change its state.

In my experience I have found that when an app reaches a certain level of complexity, and its source code is split across a large number of files, explicitly expressing dependencies between different parts of the source code is a tremendous help to understanding the structure of the code and the relationships of the different parts of the app.

I have also come to appreciate the possibility of expressing interfaces not only in terms of classes and modules, but also with procs/lambdas (i.e. closures) and other singleton objects. In many cases you’ll need to access some “global” service or state object, such as a database connection pool, a background job store, a mailer object, or even a configuration hash. Being able to do that with explicit references means that you don’t need to rely on these objects being available as global constants. This in turn makes it easier to implement dependency injection, and it also greatly simplifies testing.

Code Organization in Syntropy

Syntropy is based on the idea that the app’s directory structure reflects its URL namespace. For example, a simple blog app would have a directory structure resembling the following:

files               URLs
=====               ====
+ app/
  + _lib/
    + storage.rb
  + _layout/
    + default.rb
  + index.rb        /
  + about.rb        /about
  + posts/
    + [id]/
      + index.rb    /posts/[id]
      + edit.rb     /posts/[id]/edit
    + index.rb      /posts
    + new.rb        /posts/new

The example above consists of both controller code (which maps to URLs) and internal dependencies which are typically put in directories such as _lib or _layout (any directory or file whose name starts with an underscore is considered internal and is not exposed by the Syntropy router).

How do those different source files get loaded by the application? In the case of controllers, they’re loaded automatically when the server receives a request with a URL that matches the source file location. For example, a GET /posts request will be routed to app/posts/index.rb.

A second principle in Syntropy is that all dependencies are explicit. In order to load any dependency inside the app, we call import. For example, here’s how the posts index controller may look:

# app/posts/index.rb
@storage = import '/_lib/storage'
@layout = import '/_layout/default'
@template = @layout.apply { |posts:, **|
  posts.each { |post|
    article {
      h1 { a post.title, href: post.url }
      p post.body
    }
  }
}

export ->(req) {
  posts = @storage.get_all_posts
  req.respond_html(@template.render(posts:))
}

In the above example, we import two dependencies, a @storage object which serves as the model interface, and a @layout object which serves as a Papercraft layout template. We then create a template that renders a list of posts, and finally we export the controller lambda, which takes an incoming HTTP request, retrieves the list of posts from @storage renders an HTML response by rendering @template.

Since we can export any object from a module, and that will serve as its value, we might express the layout module as follows:

# app/_layout/default.rb
export template {
  html {
    head { ... }
    body {
      render_children
    }
  }
}

In this module the layout template (a Papercraft template) is the actual interface of the module, such that each time you import it, you get the same object that was exported. Here’s how the storage module might look:

# app/_lib/storage.rb
export self

def get_posts
  ...
end

...

In the case of the storage module, we export the module itself as a singleton, which lets us call any of its methods:

storage = import '/_lib/storage'
storage.get_posts

Loading code in this way has some important advantages:

Injecting State into Modules

Since each module is evald in a separate, anonymous context, this means we can also easily inject state into the module when loading it. This is especially useful for injecting an environment hash (holding the app’s configuration), or other app concerns that may be used by the module, such as the app object itself, the underlying UringMachine instance, or any other global service. This too may simplify the task of unit testing those modules. This is done by setting instance variables that are available to the module. Here’s how a Syntropy module may access the app’s configuration:

# app/_lib/mailer.rb
require 'my_mailer'

export MyMailer.new(@env[:config][:smtp_server])

Here, we access @env, which is injected into the module when it’s loaded. And of course, we could easily inject a custom @env object if we wanted to, for testing purposes.

How Module Loading Works in Syntropy

In the previous article presenting Syntropy, I discussed the basic mechanics of how Syntropy loads modules. Let’s look in more detail at how Syntropy achieves this. The entire Syntropy module loader code is completely self-contained and does not depend on any other part of Syntropy, it just needs to be provided with a few dependencies that are passed to the modules being loaded.

# Greatly simplified version:
class ModuleLoader
  def initialize(env)
    @env = env
    @app_root = env[:app_root]
    @modules = {} # maps ref to module entry
  end

  def load(ref)
    ref = "/#{ref}" if ref !~ /^\//
    if !(entry = @modules[ref])
      entry = load_module(ref)
      @modules[ref] = entry
    end
    entry[:export_value]
  end

  private

  def load_module(ref)
    fn = File.expand_path(File.join(@app_root, "#{ref}.rb"))
    code = read_file(fn)
    env = @env.merge(module_loader: self, ref: clean_ref(ref))
    mod = ModuleContext.new(env, code, fn)

    {
      fn: fn,
      module: mod,
      export_value: mod.__export_value__,
    }
  end
end

The module loader is initialized with an env object representing the app’s environment (including its configuration). The #load method reads the source code for the module, then instantiates a ModuleContext object that’s going to serve as the context in which the module’s source code is going to run:

# Greatly simplified version:
class ModuleContext
  def initialize(env, code, fn)
    @env = env
    @module_loader = env[:module_loader]
    @ref = env[:ref]
    instance_eval(code, fn)
  end

  private

  def export(v)
    @__export_value__ = v
  end
  
  def import(ref)
    @module_loader.load(ref)
  end
end

The entire mechanism of module loading is represented in these few lines: we create a module context instance, evaluate the source code using #instance_eval, and the import/export methods are defined as private methods and allow explicit dependencies between the different modules, and the usage of any object as a module interface.

Here’s how we may use this in a testing situation:

class FooTest < Minitest::Test
  def setup
    @module_loader = ModuleLoader.new(
      # injected environment hash
      {
        app_root: File.join(__dir__, 'fixtures')
        foo: 'foo'
      }
    )
  end

  def test_foo_module
    foo = @module_loader.load('foo')
    refute_nil foo

    # suppose the foo module has an #env method:
    assert_equal 'foo', foo.env[:foo]
  end
end

As you can see in the code above, the fact that there’s no global state makes writing tests so much easier: our tests do not have a side-effects (at least in terms of changing the global namespace), and we can inject arbitrary values into the environment passed to the loaded modules. Also, since the interfaces for the ModuleLoader and ModuleContext classes are so minimal, it’s easy to extend or compose them into more complex behaviours, or simply to override the stock behaviour.

Breaking Out of the OOP Jail

If there’s one thing I adore about Ruby is the fact that it’s so flexible and so amenable to different styles of writing code and to different techniques. The common approach to writing Ruby code is based on sticking to more or less strict OOP principles: encapsulation, inheritance, code organized into classes, and classes organized into a single global topology.

But there are other ways to write Ruby code - and a lot of times, expressing a functionality as a closure (i.e. a Proc) is so much more powerful, expressive and concise than the usual approach. This is especially true when we’re dealing with building apps on top of a web framework. The framework expects the app’s controllers to have a certain shape, but instead of expressing those controllers as classes, we can express them as procs:

# The Rails way:
class PostsController < ApplicationController
  def index = ...
  end
end

# The Syntropy way
export ->(req) { ... }

There are important differences here, some of them were discussed above, and the question of boilerplate is actually the least interesting one. The Rails controller is globally named, while the Syntropy controller is anonymous. In Rails, an instance of the controller is allocated for each request and its entrypoint method is then called. In Syntropy the controller is the entrypoint that’s called on each request (with the request as an argument), and there’s no object allocation when the controller is invoked.

Also, the fact that controllers are procs gives us both freedom to organize our code as we want, and an almost magical ability to dynamically generate custom controllers, using a variety of techniques. Lastly, using closures (i.e. procs) as units of modularity encourages us to create clean interfaces. When the surface area of a certain functionality is reduced to a single method interface (i.e. #call), you start thinking differently about how to build your app.

Take a look at a controller from the blog example in the Syntropy repository. This is a barebones site that allows you to view, add, edit and delete blog posts:

# app/posts/index.rb
@posts = import '_lib/posts'
@layout = import '_layout/default'
@template = @layout.apply {
  ...
}

export dispatch_by_http_method

def get(req)
  posts = @posts.get_all
  req.respond_html(@template.render(posts:, flash: req.flash))
end

def post(req)
  data = req.get_form_data
  title = req.validate(data['title'], String, /.+/)
  body = req.validate(data['body'], String, /.+/)
  id = @posts.create(title, body)

  req.flash[:notice] = 'Post was successfully created.'
  req.redirect("posts/#{id}")
end

We start by importing dependencies and assigning them to instance variables: @posts is the model, @layout is the Papercraft template used as layout, and @template generates HTML to show the list of posts. We then export an expression: dispatch_by_http_method. This is actually a method call, here’s its implementation:

def dispatch_by_http_method
  ->(req) {
    sym = req.method.to_sym
    raise Syntropy::Error.method_not_allowed if !respond_to?(sym)

    send(sym, req)
  }
end

Here we get to taste some of the power of using procs as closures: we generate a closure that knows how to dispatch a request based on the HTTP method by calling the corresponding Ruby method on itself. It is automatically bound to the module context (i.e. self), and with just a few lines of code we expressed a very common behaviour - the request will be dispatched to the method corresponding to the request’s HTTP method, and if the method is not implemented, a METHOD NOT ALLOWED status code is returned to the client.

So once we call dispatch_by_http_method to generate the controller’s entrypoint, all that’s left is to implement the handlers for the HTTP methods we want to accept, namely GET and POST:

export dispatch_by_http_method

def get(req)
  ...
end

def post(req)
  ...
end

We could in fact use the same technique to implement a Rails-style controller (it will need to be a wildcard route because a Rails controller actually handles multiple different URLs by default):

# app/posts+.rb
export dispatch_action

def index(req) = ...

def show(req) = ...

def new(req) = ...

# €tc...

Now take a look at the code above. To quote a famous Rails person, “look at all the things I’m not doing!” I’m not defining any classes, I’m not namespacing anything, I’m defining global constants. The only reference to this code is its location in the app’s directory structure, and the dispatch_by_action directive dynamically creates a closure that wraps our code, along with a little bit of logic to dispatch the request to the correct method.

So, while we still organize our controller logic using methods, the glue code, is implemented using procs/closures. Now I’m not saying that all Ruby code should look like this. Of course there are many situations where the classic OOP approach is beneficial. This is especially true for Ruby gems, where we need to have clearly defined APIs, and also some type information. But in an app-on-top-of-a-framework situation, where the app code is driven by the framework, coding the app’s entrypoints as closures brings with it multiple benefits: less boilerplate, clearer dependencies, more dynamic behaviour, and expressivity.

I think there’s still a lot to be discovered in writing Ruby code in a more functional style. In the last few years I’ve been exploring just that - bringing a more functional approach to Ruby. Some of my work has lead to the development of Papercraft, which brings a functional approach to HTML templating. Syntropy now brings a functional approach to web controllers. And I think the same could also be done for models, which I hope to discuss in a future article.