The term “leaky abstractions” has been around for a while. Coining it is most often attributed to Joel Spolsky, who wrote this often-cited article about it. I’ve now stumbled upon another interpretation of a leaky abstraction, measured by the depth of a stack trace:

So, long stack traces are bad according to Geek & Poke. I’ve seen this argument before on Igor Polevoy’s blog (he’s the creator of ActiveJDBC, a Java implementation of the popular Ruby ActiveRecord query interface). Much like Joel Spolsky’s argumentation was often used to criticise ORMs, Igor’s argument was also used to compare ActiveJDBC with Hibernate. I’m citing:
One might say: so what, why do I care about the size of dependencies, depth of stack trace, etc. I think a good developer should care about these things. The thicker the framework, the more complex it is, the more memory it allocates, the more things can go wrong.
I completely agree that a framework with a certain amount of complexity tends to have longer stack traces. So if we run these axioms through our mental Prolog processors:
- if Hibernate is a leaky abstraction, and
- if Hibernate is complex, and
- if complexity leads to long stack traces, then
- leaky abstractions and long stack traces correlate
I wouldn’t go as far as claiming there’s a formal, causal connection. But a correlation seems logical.
But these things aren’t necessarily bad. In fact, long stack traces can be a good sign in terms of software quality. It can mean that the internals of a piece of software show a high amount of cohesion, a high degree of DRY-ness, which again means that there is little risk for subtle bugs deep down in your framework. Remember that a high cohesion and high DRY-ness lead to a large portion of the code being extremely relevant within the whole framework, which again means that any low-level bug will pretty much blow up the whole framework as it will lead to everything going wrong. If you do test-driven development, you’ll be rewarded by noticing immediately that your silly mistake fails 90% of your test cases.
A real-world example
Let’s use jOOQ as an example to illustrate this, as we’re already comparing Hibernate and ActiveJDBC. Some of the longest stack traces in a database access abstraction can be achieved by putting a breakpoint at the interface of that abstraction with JDBC. For instance, when fetching data from a JDBC ResultSet.
Utils.getFromResultSet(ExecuteContext, Class<T>, int) line: 1945 Utils.getFromResultSet(ExecuteContext, Field<U>, int) line: 1938 CursorImpl$CursorIterator$CursorRecordInitialiser.setValue(AbstractRecord, Field<T>, int) line: 1464 CursorImpl$CursorIterator$CursorRecordInitialiser.operate(AbstractRecord) line: 1447 CursorImpl$CursorIterator$CursorRecordInitialiser.operate(Record) line: 1 RecordDelegate<R>.operate(RecordOperation<R,E>) line: 119 CursorImpl$CursorIterator.fetchOne() line: 1413 CursorImpl$CursorIterator.next() line: 1389 CursorImpl$CursorIterator.next() line: 1 CursorImpl<R>.fetch(int) line: 202 CursorImpl<R>.fetch() line: 176 SelectQueryImpl<R>(AbstractResultQuery<R>).execute(ExecuteContext, ExecuteListener) line: 274 SelectQueryImpl<R>(AbstractQuery).execute() line: 322 T_2698Record(UpdatableRecordImpl<R>).refresh(Field<?>...) line: 438 T_2698Record(UpdatableRecordImpl<R>).refresh() line: 428 H2Test.testH2T2698InsertRecordWithDefault() line: 931
Compared to ActiveJDBC’s stack traces, that’s quite a bit more, but still less compared to Hibernate (which uses lots of reflection and instrumentation). And it involves rather cryptic inner classes with quite a bit of method overloading. How to interpret that? Let’s go through this, bottom-up (or top-down in the stack trace)
CursorRecordInitialiser
The CursorRecordInitialiser is an inner class that encapsules the initialisation of a Record by a Cursor, and it ensures that relevant parts of the ExecuteListener SPI are covered at a single place. It is the gateway to JDBC’s various ResultSet
methods. It is a generic internal RecordOperation
implementation that is called by…
RecordDelegate
… a RecordDelegate
. While the class name is pretty meaningless, its purpose is to shield and wrap all direct record operations in a way that a central implementation of the RecordListener SPI can be achieved. This SPI can be implemented by client code to listen to active record lifecycle events. The price for keeping the implementation of this SPI DRY is a couple of elements on the stack trace, as such callbacks are the standard way to implement closures in the Java language. But keeping this logic DRY guarantees that no matter how a Record is initialised, the SPI will always be invoked. There are (almost) no forgotten corner-cases.
But we were initialising a Record in…
CursorImpl
… a CursorImpl, an implementation of a Cursor. This might appear odd, as jOOQ Cursors are used for “lazy fetching”, i.e. for fetching Records one-by-one from JDBC.
On the other hand, the SELECT
query from this stack trace simply refreshes a single UpdatableRecord, jOOQ’s equivalent of an active record. Yet, still, all the lazy fetching logic is executed just as if we were fetching a large, complex data set. This is again to keep things DRY when fetching data. Of course, around 6 levels of stack trace could have been saved by simply reading the single record as we know there can be only one. But again, any subtle bug in the cursor will likely show up in some test case, even in a remote one like the test case for refreshing records.
Some may claim that all of this is wasting memory and CPU cycles. The opposite is more likely to be true. Modern JVM implementations are so good with managing and garbage-collecting short-lived objects and method calls, the slight additional complexity imposes almost no additional work to your runtime environment.
TL;DR: Long stack traces may indicate high cohesion and DRY-ness
The claim that a long stack trace is a bad thing is not necessarily correct. A long stack trace is what happens, when complex frameworks are well implemented. Complexity will inevitably lead to “leaky abstractions”. But only well-designed complexity will lead to long stack traces.
Conversely, short stack traces can mean two things:
- Lack of complexity: The framework is simple, with few features. This matches Igor’s claim for ActiveJDBC, as he is advertising ActiveJDBC as a “simple framework”.
- Lack of cohesion and DRY-ness: The framework is poorly written, and probably has poor test coverage and lots of bugs.
Tree data structures
As a final note, it’s worth mentioning that another case where long stack traces are inevitable is when tree structures / composite pattern structures are traversed using visitors. Anyone who has ever debugged XPath or XSLT will know how deep these traces are.
The ideal stack trace is the one which, if you read it in a log, gives you already just by reading quite a good idea what exactly went wrong.
This leads to the following basic principle:
One class, one method just does one thing, which is reflected in its name, which should be as specific as possible. A good verifiable guideline for that is the very simple rule that a method should always be fully readable on a standard-sized screen (ie. less than 50 lines of code)
Coding according to this principle alone forces you to refactor as a method grows too big, thus having to think about its naming again.
But, also, it leads to deep stack traces.
Now, taking the example in your referenced blog post. In the Hibernate example, I know roughly what went wrong. On the other hand in the ActiveJDBC example, all the trace is giving me are three very generic function names.
Good point. I wasn’t getting into the naming aspect, but that’s totally true. And again, your arguments are arguments in favour of high cohesion, which is good.
To be fair, in this case, I’d say that the error reported by the JDBC driver is clear enough. ActiveJDBC API usage went wrong in SelectAllActiveJDBC.java at line 13. And I think it’s actually easier to know what went wrong in the ActiveJDBC stack trace, because the actual error is closer to the stack trace element that caused it, than in the Hibernate example.
Lukas, we meet again on the Intertubes :). I like your write up, it makes sense. Of course, what I stated needs to be taken with a grain of salt:) The depth of stack is an indicator, and if a framework is “well designed, and refactored”, then the stack trace shows cohesion.
However, I was merely using a stack trace as one of the indicators to judge “thickness” of a framework. As such, poorly architected or well architected.. if the stack trace is deep, then I would say the framework is thick. Especially if you start seeing methods on the stack trace that are dependencies, intertwined with the framework functions:).
Personally, I prefer thin frameworks (scratched my own itch by developing ActiveJDBC and ActiveWeb), since they generally provide no surprises and just work.
I agree that a God Object anti-pattern will create the tinnest line stack trace :).
At the same time, I think that: “Deep Stack Traces Can be a Sign for Good Code Quality” .. but this is more an exception to the rule.
Yes, absolutely. There is a certain amount of inevitable stack length in “thicker” frameworks.
Thus: can be :-)
I wrote a one page site that has code folding and some colouring so that I can read really big Java stack traces from time to time:
http://www.stacktidy.com/
Thanks for sharing. That looks pretty useful, indeed!