Ryan Harrison My blog, portfolio and technology related ramblings

Mocking Time in Java

If your code calls LocalDateTime.now() or Instant.now() directly, you’re stuck with whatever the system clock says. This makes it nearly impossible to write deterministic tests for time-sensitive logic like expiration checks, scheduling, or time-based calculations.

The solution is to inject time as a dependency rather than calling static now() methods directly. Java’s Clock abstraction provides a clean way to do this, and there are several approaches depending on your needs.

The Problem with Static now() Calls

Consider a service that checks if a subscription has expired:

public class SubscriptionService {
    public boolean isExpired(Subscription subscription) {
        return subscription.getExpiryDate().isBefore(LocalDateTime.now());
    }
}

This code is difficult to test because LocalDateTime.now() always returns the actual current time. You can’t easily test the boundary conditions around expiration without changing your system clock or waiting for time to pass.

Read More

Git - Disable autocrlf

If you’re on Windows, Git has almost certainly been silently messing with your line endings since the day you installed it.

TLDR: The fix is this one command:

git config --global core.autocrlf false

It’s on by default. It’s a terrible default. Mainly due to historical reasons that no longer exist.

Read More

Engineering Guidelines Site

I’ve put together a reference site for engineering best practices, available at guidelines.ryanharrison.co.uk. The goal is a single place to find guidelines and conventions covering the full stack - from core principles through to deployment.

Rather than being prescriptive about specific tools, most sections aim to explain the reasoning behind a recommendation so you can apply it to your own context.

What’s Covered

Principles and practices - Core engineering principles covers the fundamentals: SOLID, DRY, KISS, YAGNI, clean code, and when to apply them. There’s also a code review guide and sections on technical debt, pull requests, and git workflow.

Architecture - Patterns for microservices, event-driven systems, and multi-tenancy.

API design - REST fundamentals and patterns, GraphQL, and OpenAPI contract-first development.

Testing - The testing strategy page covers the overall approach. From there, individual pages go into unit, integration, contract, end-to-end, mutation, and chaos testing.

Security and observability - Security overview covering authentication, authorisation, input validation, and data protection. Observability covers structured logging, metrics, tracing, and alerting.

Languages and frameworks - Guidelines for Java, Kotlin, TypeScript, and Swift, with framework-specific sections for Spring Boot, React, Angular, React Native, Android, and iOS.

Infrastructure - Docker, Kubernetes, Terraform, and a fairly detailed AWS section covering compute, networking, storage, EKS, and more.

Read More

Building Jekyll Sites with Docker

This site is built with Jekyll. That unfortunately often means wrestling with Ruby versions, gem dependencies, and environment configuration. Different machines require different setups, and what works on your machine might not work in CI or on your VPS. Docker solves this for many other areas by providing a standalone and reproducible build environment, so why not here as well?

The challenge is that the official Jekyll Docker images are no longer being actively maintained. Thankfully, there’s a decent community alternative that handles modern Jekyll sites without the maintenance burden.

The bretfisher/jekyll-serve image is well-maintained (for now) and works as a drop-in replacement for the official Jekyll images. By default, it serves your site locally via jekyll serve, but you can override the command to run any Jekyll operation you need.

The image handles all the Ruby and gem setup for you, so you don’t need to worry about version conflicts or system dependencies.

Local Development with Docker Compose

For local development, the fastest approach is using Docker Compose. Create a compose.yml file in your Jekyll project root:

services:
  jekyll:
    image: bretfisher/jekyll-serve
    volumes:
      - .:/site
    ports:
      - "4000:4000"

Start your development server with:

docker compose up

Your site will be available at http://localhost:4000 with live reload enabled. The key benefit here is that Docker Compose reuses the same container across runs, which caches your gems. This means subsequent starts are very quick - typically just a few seconds once the gems are installed.

Without compose, you can achieve the same result with:

docker run -p 4000:4000 -v $(pwd):/site bretfisher/jekyll-serve

However, this creates a new container each time, which means reinstalling gems on every run (unless you mess around with more volume mounts, see below).

Building for CI/CD

For continuous integration or deployment builds, you want to build the static site without running the server. Override the default command to run the Jekyll build process:

docker run -v $(pwd):/site bretfisher/jekyll-serve bundle exec jekyll build

This generates your static site into the _site directory. Here’s an example GitHub Actions workflow that builds on every push (also the one used to build this site):

name: Build Jekyll Site
on:
  push:
    branches: [ master ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Build with Jekyll
        run: |
          docker run -v $:/site \
            bretfisher/jekyll-serve bundle exec jekyll build

The build is completely reproducible because the Docker image contains a known-good version of Ruby and all the necessary build tools.

Building on Low Memory Machines

If you’re like me and building Jekyll sites on machines with limited memory (1GB or less), you might run into issues. Gem installation often requires building native extensions, which can be memory-intensive. The bretfisher/jekyll-serve image runs bundle install --retry 5 --jobs 20 by default, which parallelizes gem installation, but uses more memory. This caused issues for me on resource-restricted boxes.

You can override the entrypoint to reduce the number of parallel jobs:

docker run -v $(pwd):/site \
  --entrypoint /bin/bash \
  bretfisher/jekyll-serve \
  -c "bundle install --jobs 2 && bundle exec jekyll build"

Reducing --jobs from 20 to 2 significantly reduces memory usage during the gem installation phase. The build will take a bit longer, but it won’t crash on memory-constrained systems.

Read More

Remote Debugging Java Apps with IntelliJ

A junior developer recently came to me for help with one of those classic issues which appears on a remote dev environment, but for whatever reason can’t (at least easily) be replicated locally. Their immediate thought was to add more log statements and redeploy the app to see what’s going on. Not unreasonable, but this is a dev environment, so I connected IntelliJ to one of the running containers and began stepping through the code and inspecting variables. They looked at me thinking I was performing some kind of black magic, so here’s an intro or a quick reminder of something which goes very underappreciated.

Why Remote Debug?

There are a few general scenarios when you might reach for remote debugging:

  • Environment-specific bugs - Issues that only appear in staging, testing, or production-like environments with specific configurations, data, or network conditions
  • Container debugging - When your application runs inside Docker containers or Kubernetes pods and you need to debug without rebuilding images
  • Shared development environments - Debugging applications running on shared development servers or VMs
  • Integration testing - Troubleshooting complex integration scenarios with external systems that can’t be replicated locally

Rather than relying solely on log statements or trying to recreate production conditions locally, remote debugging lets you step through the actual running code in the target environment - a lot better than adding log statements!

How It Works

Java remote debugging uses the Java Debug Wire Protocol (JDWP), which is a communication protocol between a debugger and a Java Virtual Machine. The JVM opens a socket that a debugger can connect to, allowing it to control execution, set breakpoints, inspect variables, and evaluate expressions.

The JVM can act as either a server (listening for debugger connections) or a client (connecting to a debugger). In most cases, you’ll configure the JVM as a server and then have your IDE connect to it.

Enabling Remote Debugging

To enable remote debugging, you need to pass specific JVM arguments when starting your Java application. The modern syntax (Java 9+) looks like this:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar myapp.jar

Here’s a high-level breakdown of what each parameter does:

  • -agentlib:jdwp - Loads the JDWP agent library for debugging
  • transport=dt_socket - Uses socket transport for the debug connection (the standard approach)
  • server=y - Configures the JVM to listen for debugger connections rather than connecting out to a debugger
  • suspend=n - Starts the application immediately without waiting for a debugger to attach. Use suspend=y if you need to debug startup code
  • address=*:5005 - Binds to all network interfaces on port 5005. You can specify a specific IP address or hostname instead of *

For older Java versions (Java 8 and earlier, so I hope you won’t see this), you might see the older syntax:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar myapp.jar

This accomplishes the same thing but uses deprecated flags. Note that in Java 8, the address parameter only accepts a port number, not the *:port format.

Read More