My world is that of startups.
I’ve been working at early stage companies or starting them for almost fifteen years now. So my worldview is a bit colored by the needs of startups, but I submit that these needs translate to those of any organization faced with the challenge of delivering a relevant product quickly to its customers.
Eric Ries in the Lean Startup tells us that we can measure a startup’s runway by the number of pivots it has left.
I really like this view. Let’s extrapolate that to applications. Applications that are easy to change are easy to pivot. It’s easy to throw away this bit of code and add this other bit over here.
As a startup, or in fact as an any organization that wants to stay lean, and to be able to quickly test ideas, good software architecture that enables many pivots (read: easy to change) is extremely important.
Even if you have figured out what you’re building on a macro scale (here at Reverb.com, we’re building a curated marketplace for musical gear), you’re still doing lots of pivots on a micro scale - building experimental features, enhancing the ones that need help, and killing off ones that don’t work.
I often hear people talk about the idea that startups should take on lots of technical debt in order to ship quickly.
I think this is just nonsense. The technical debt you take on is reducing the number of pivots you have left and thus killing the chance that your company will make it.
Now, should you engineer everything to the n-th degree? Should you aim for zero tech debt? Should you build a 20-layer architecture fully decoupled from everything including your database, and launch a swarm of microservices with a team of five?
Of course not. Tech debt, like real debt, is a powerful instrument for growth. There are practical limits to what a small team can do while delivering business value. And usually this means you’ll be building a monolithic app to start.
Are monoliths inherently bad?
No; they enable us to build quickly and keep operational complexity to a minimum. But the way you go about it is important.
A monolithic app does not mean you have to stuff all your code into thousand-line model classes. That’s not a shortcut, that’s a surefire way to prevent the scalability of your team. Large classes become larger until they’re so massive that they’re black holes, causing every bit of the system and every team member to depend on their structure.
Refactoring becomes progressively harder, and development is slowed to a halt. Frustrated engineers start to leave and new ones don’t want to deal with legacy cruft; the team churns, the company fails. So what are we to do?
SRP and OCP to the rescue!
During the last couple years, it has become increasingly apparent to me that the Single Responsibility Principle (that is, that objects should change for only one reason), and the Open/Closed Principle (that we should not have to edit existing code in order to add functionality) are the building blocks of our salvation.
The missing piece of the puzzle hit me on the head like a brick when I watched Uncle Bob’s Ruby Midwest Keynote, Architecture: The Lost Years. In this talk, Uncle Bob mentions but does not go into details on the idea of the Use Case, which comes from Ivar Jacobson all the way back in ‘92.
I’ll save you a 500 page read: the idea is basically just to reify your behaviors.
Instead of stuffing behaviors as methods into your objects like classical OOP seems to tell us to do, Jacobson suggests creating Use Cases like CreateAccount or ProcessOrder.
And it seems that besides Uncle Bob, other very smart people are talking about similar concepts. In 2009, James Copelien and Trygve Reenskaug (inventor of MVC) put together a paper outlining a new architecture called Domain, Context, Interaction (DCI) which started getting attention in the Rails community probably around 2012 (at least that’s when I started seeing blogs about it). Cope and Trygve later wrote a book called Lean Architecture.
DCI’s simple beauty is obscured by the red herring of runtime behavior extension
For some reason people started talking about DCI’s novel idea of extending behavior onto objects at runtime, and Rubyists starting hacking together all kinds of fun things, from extending modules onto objects at runtime, to using refinements, to frameworks that help you extend and unextend behaviors onto objects.
But in fact, the main revelation of Use Cases and DCI is that you reify your behaviors by giving them recipe-like classes of their own, and specifically for DCI, the other key concept is that you wrap your domain objects (models) with additional behavior called Roles that is relevant only in particular contexts (use cases).
You don’t have to rely on special language tricks to do this.
A humble decorator/delegator works just fine. So you can have a use case like ProcessOrder which takes an Order object and wraps it with a SimpleDelegator called TaxableOrder so that you can calculate the tax.
That’s it, in a nutshell.
I personally think it’s sad that such beautifully simple concepts are hidden in really long books that most people with today’s TLDR attention span will never read. So here’s hoping that more people will be joining the discussion on how we can keep our code small, simple, and low churn by applying these principles.
TLDR: reify your behaviors into Use Cases, wrap your domain objects with Roles to aid in the use cases they participate in. Use Cases are inherently write-once and don’t change unless the business process changes, thus they strongly support SRP and OCP, and keep your classes small and single purpose, preventing churn and growth in the underlying domain objects. Low churn code is easy to refactor and change, and thus it’s easy to keep the business moving forward with lots of pivots and experiments.
If you found this interesting, please come see my talk at Windy City Rails, 2014 and chat me up in the breaks!