Decision Log
Document design and architecture decisions.
A lighter-weight alternative to Architecture Decision Record (ADR).
Testing Revamp (Jan 2025)
In earlier versions of Pedestal, the io.pedestal.test
namespace directly contained reify
-ed mock
implementations of the relevant Servlet API types: HttpServletRequest
, HttpServletResponse
, and so forth.
To some degree, these were abuses of reified types, because almost none of the mocks fully implemented all the necessary methods [1]; further, because of limitations to reified types, they did not always inherit common behavior from base classes provided by the Servlet API.
By converting these to full Java mock implementations, it is more clear that the mocks match the Servlet API, as failure to do so result in a compilation error. It also makes it clear what is, and is not, implemented on a method-by-method basis. In addition, the Servlet API is, by design, filled with mutable state - managing that mutable state inside a Java class is far simpler than doing so in Clojure code.
We feel that the end result is more complete and maintainable, at the cost of adding several large (and mostly empty) Java classes to the build.
Using this approach, the FnServlet
was also converted from reified type to a Java class; this class
is provided by tfhe servlet
function, and in an embedded Pedestal application,
represents the initial bridge from the Java Servlet API to the Clojure and Pedestal implementations.
Resource Routes (Jan 2025)
Routing is a central concept in Pedestal, and so it causes friction when other important concepts are inconsistent with it. One common example is how Pedestal makes file system or class path resources available; this happens outside of routing - by default, it occurs just before the router interceptor in the default interceprtor stack.
Having resources operating outside of routing means that there can be conflicts between resources and routes that go undiagnosed. It also means that it is not possible to reverse the URI for a resource in the way that is possible for endpoints defined in the routing table.
In addition, exposing resources as routes has opened up new paths for caching, leading to greater efficiency when serving such resources.
Whether support for the interceptor-based approach for resources remains fully supported, or is deprecated, is an outstanding question.
-
The route-based resources are demonstrably faster than interceptor-based.
Jetty 12 (Dec 2024)
Although Pedestal 0.7 bumped the supported version of Jetty from 9 to Jetty 11, this was only an interrum solution - Jetty 9 ended community support in June 2022, with EOL slated for Feb 2025, but Jetty 11 ended community support in Jan 2024, and EOL in Jan 2025.
Thus, there is no reason to continue supporting both Jetty 11 and Jetty 12; Jetty 12 does not yet have a scheduled EOL.
So, although initial plans were to support both 11 and 12 in Pedestal 0.8, we have pivoted to only supporting Jetty 12.
Routing Fragments (Fall 2024)
The original intention of the routing table was that an application would construct a single routing specification at startup; this would be converted to a routing table and combined with a router to form a routing interceptor.
This is sufficient for many small projects using Pedestal; for larger users, such as the vast number of services deployed at Nubank, it has proven insufficient.
Nubank services are built on common libraries; both the application and library layers each contribute portions of the routing table; this is a haphazard process in Pedestal 0.7 and earlier, because certain APIs assume just a single routing specification for the entire application.
In Pedestal 0.8, this was reworked (sacrificing some backwards compatibility) to allow for routing fragments, that can be properly combined, verified for correctness, and utilized by the framework.
Sawtooth Router (Sep 2024)
A common query internally at Nubank, when asked "How can we make things better?" was a call for help with routes, particularily identifying routing conflicts.
The existing router implementation, the Prefix Tree Router had specific rules for dealing with any conflicts, but did a bad job of emitting warnings about such conflicts.
Sawtooth was created to address these concerns; it’s behavior when conflicts occur is not defined, but it was designed to do a good job of identifying routing conflicts.
Although the original goal was to create a router that was as fast, or faster, than prefix tree, that turned out to be difficult to achieve [2]; Sawtooth is nearly as fast as Prefix Tree, with a difference in micro-seconds per routing execution.
The final decision was whether to sawtooth the default router; this seemed acceptible give its reasonable performance, and improved ergonomics. The other routers, prefix tree included, continue to exist for backwards compatibility reasons, and to support cases where Sawtooth is not the best fit.