Dependency Minimalism

  • jvm
  • AWS

intro

In launching Service Scheduler for JSM, I wanted to look back on a coding practice that only really started to think about a few projects ago, but that I've been using more intentionally, since. This practice is about being intentional and informed about dependency usage, and aiming to reducing code dependencies. I've never heard this practice named, but for the time being I have inconveniently decided to call it _dependency minimalism.

I this post, I'll walk you through how I used this practice to reduce my deployable back-end JVM application bundle from 24MB down to an even smaller number.

rules of dependency minimalism

NOTE: before we continue, I want to clarify two terms I will use bellow: Framework and Library.

I use the term Framework to describe a dependency that requires you to structure your code to fit into the framework. Frameworks almost all require you to implement an interface or extend some base class, and connect your code to their runtime.

Libraries are dependencies that do a specific thing, that can be consumed from anywhere in your code.

  1. before adding a dependency, ask yourself what problem you are trying to solve and is it a complicated problem to solve.
  2. if you decide to add a dependency 1.1. use dependencies with fewest possible transitive dependencies, or favor libraries that use the same transitive dependencies to the ones you are already pulling in.

early project

range of functionality

This application is deployed as a set of four AWS Lambdas, each from the same application bundle, and serving the roles of HTTP endpoint (api-gateway), worker (SQS consumer), scheduled jobs and database migrator.

The application is not a big ball of mud, but consists of a layerd-architecture with application modules that initialize runtime components either as targets for different lambda operations, or as a re-usable service for other modules.

Some simple clean code practices are used such as wrapping primitive types and SOLID.

The application domain uses Patterns of Enterprise Applications ( see this catalog), and applies a number of code patters from Domain Driven Design to clearly identify domain entities, their life-cycle, factory methods, services, aggregate-roots. The domain model consists of encapsulated objects (no getters or setters), that enforce domain integrity and can only be committed to the database in a coherent state.

The read model is separate from the domain model - applied Command-Query Responsibility Segregation. Some query builder patterns and such and mapping layers, allow me to optimize the fetch operations. In many cases when testing locally HTTP traffic to load data into the Single Page App takes 10ms round-trip.

The application integrates with Jira cloud using Atlassian connect, implementing the necessary JWT and lifecycle events as well as making secure API call to Jira.

The application integrates with Google Calendar, implementing the OAuth handshake, handling push notification from the calendar and making API calls the the Google Calendar APIs.

The application loads its runtime configuration from a secured AWS SSM Parameter Store - this includes database connection info, CDN info and other application runtime configuration. When running locally, these get pulled from a local env file.

All that to say, I've crammed some real code in here, this is not a hello world sample.

why didnt I use ...

There is a pretty obvious catalog of libraries and frameworks I could have used to make this easier, but chose not to. Here is a list of obvious choices and why I did not use them.

spring boot

This is perhaps the most obvious choice. This framework does so much. I have used many different Spring runtimes over the years, and they are a solid choice. I decided that the two main things it provided (dependency wiring and MVC REST server) where not very complicated to implement myself.

an ORM

ORMs are another thing many developers fall back on.

In the JVM world Hibernate is king. I've used this before and have a few complaints. I find it invasive to the domain, no matter how much I try to apply the mapping to the domain, I often hit cases where I have to adapt the ORM. I dont like the lazy-loading and similar possible side-effects. I have found it un-necessarily complicated to have different read and write models on the same sets of tables (see CQRS). I often find myself consulting the documentation.

From the above you could conclude that I must not master it all that well, and you'd be right. However it also speaks to the fact that it is a powerful, and complicated tool. At least its complicated given that I know exactly what I want to do.

I have also used JDBI which feels more like JDBC and SQL helper functions combined with an object mapper. This one is very nice. Its lean and you get much closer the the DB.

In the end I did not feel that any of these added much value. JDBC is not complicated, and with a handful of helper functions and objects, we get unfiltered control over our DB access.

a JWT library

This is a silly one. There are good JWT libraries out there. I've used them. The one thing that disappointed me was that I still needed to understand how JWT worked, and from there it took so little effort to implement what I needed in a single class that adding the dependencies was pointless. Seriously guys, its JSON and some hashing and encryption amd the specifications are well defined.

an HTTP client library

Again, there exist great HTTP client libraries for Java. The Apache client is probably my favorite. Its a shame that this functionality has not become a standard part of the JVM runtime libraries, as it is in many other languages. Still, HTTP is not complicated, so a single wrapper class gave me all I needed here.

initial dependencies

Application dependencies where:

Total deployable bundle size: 23mb

Given that I've already discarded a large number of large dependencies, you may think I decided to stop there. I could have, but I didnt.

local dev experience

I wish to side-bar and discuss developer experience for a moment. You may assume that because I cut so much out, that I had an unpleasant dev experience. Not so.

I designed the code to run locally from a single "main", the application code for all four aws lambdas running in one " application server", debuggable and very quick to re-launch.

Of course I also implemented a test pyramid, unit, integration and API tests that invoked the REST API in an embedded Jetty server (my home-made application server).

The run locally but build for AWS Lambda without using heavy app servers or containers is a discussion for another time.

All in all, this is some very pleasant dev experience.

later project

things I replaces

along the way I found that some of the dependencies I had added, were not to my liking for various reasons. This gets in to territory that many would consider un-necessary, and I think that for many teams, I would not do this, but given that I

later dependencies

Total bundle size: 7mb

conclusion

This practice is not for everybody, but I've been doing it for a little while now and I'm enjoying the benefits.

My personal benefits have been:

My projects have benefited from:

I may post some future articles exploring some of these choices in greater detail.