Go Back

read

Published on: Apr 16, 2026

Programming

Change Language:

Creating Tiniest

A Modern URL Shortener with Spring Boot.

Tiniest is a fast, minimalist URL shortening service built with Spring Boot 4.0 and Java 24, written and deployed in less than a day. Granted, this is not a highly technical project, however I wanted to truly see how great (or annoying) it would be to create a small startup-ish project with probably the best enterprise ecosystem out there.


How It Works

First, let’s talk a bit about how the algorithm works.

Step 1: SHA-256 Hashing

The input URL is passed through SHA-256, a cryptographic hash function that produces a 256-bit (32-byte) digest. This ensures:

  • Determinism: The same URL always produces the same hash
  • Uniform distribution: Even similar URLs produce wildly different hashes
  • Collision resistance: It’s computationally infeasible to find two different inputs with the same hash

Step 2: Slice and Encode with Base62

Taking the full 32-byte hash would be too much; I take a slice of the first 6 bytes of the output.

These 6 bytes are then encoded using Base62 because it is URL-safe, human-readable, and more compact than hexadecimal encoding.

Finally we get short codes like “7kPv2Nm” or “xR3qLp9”, typically 8-10 characters long.


Spring Boot: Let the framework do all the work.

Java is famous for being a boilerplate-heavy programming language. This is mostly legacy criticism in my opinion. Modern Java has a lot of tools to prevent verbose code if you want!, because there are people out here that still choose to write verbose code for clarity reasons.

    // This works
    ArrayList<Integer> numbers = new ArrayList<Integer>();
    
    // This also works
    var numbers = List.of(1, 2);

On top of that, we have Spring Boot.

Spring Boot allows us to create enterprise web apps with minimal boilerplate code.

Auto configuration Magic

The entire application bootstrap? Just this:

@SpringBootApplication
public class TiniestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TiniestApplication.class, args);
    }
}

That single ‘@SpringBootApplication’ annotation enables:

  • Component scanning
  • Auto-configuration for JPA, web server, validation, and more
  • Property source configuration

Zero Boilerplate Data Access

Check the repository layer:

public interface UrlRepository extends JpaRepository<Url, Long> {
    Optional<Url> findByMinifiedPath(String minifiedPath);
    Optional<Url> findByPath(String path);

    @Modifying
    @Query("UPDATE Url u SET u.clickCount = u.clickCount + 1 WHERE u.minifiedPath = :minifiedPath")
    void incrementClickCount(String minifiedPath);
}

That’s it! No implementation class needed. Spring generates:

  • All CRUD operations automatically
  • Query methods derived from method names (‘findByMinifiedPath’, ‘findByPath’)
  • Custom update queries with ‘@Query’

REST Controllers in Minutes

Building REST APIs is almost trivial:

@RestController
@RequestMapping("/api/urls")
public class ApiController {

    @PostMapping
    public ResponseEntity<ShortenResponse> shorten(
            @Valid @RequestBody ShortenRequest request,
            HttpServletRequest httpRequest) {

        Url url = urlService.shortenUrl(request.url());
        return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(url, httpRequest));
    }

    @GetMapping("/{shortCode}")
    public ResponseEntity<ShortenResponse> getStats(@PathVariable String shortCode) {
        Url url = urlService.getUrlStats(shortCode);
        return ResponseEntity.ok(toResponse(url));
    }
}

Normally, you create controllers for each group of endpoints, aka:

  • /users
  • /urls
  • /posts

But since this was a very small project, I created a single controller that contains the only two endpoints we need.

Spring handles:

  • JSON serialization/deserialization
  • Request validation with Bean Validation
  • Path variable extraction
  • HTTP status code mapping

Coming from Node.js/JavaScript, most of this might not be that impressive, but typed languages (especially those without reflection—looking at you, Rust) can make working with JSON data more difficult.

Java Records for DTOs

Remember when I talked about Java not being as verbose as people make it to be? That’s right, we have RECORDS BABYYYYY.

Records are basically classes made for transporting immutable data; they reduce a lot of boilerplate.

public record ShortenRequest(
    @NotBlank(message = "URL cannot be blank")
    @URL(message = "Must be a valid URL")
    String url
) {}

public record ShortenResponse(
    Long id,
    String originalUrl,
    String shortCode,
    String shortUrl,
    Long clickCount,
    Instant createdAt
) {}

Records automatically provide constructors, getters, ‘equals()’, ‘hashCode()’, and ‘toString()’. Combined with validation annotations, they’re perfect for API contracts.

Sadly Records in Java don’t prevent memory allocation like in C#, but that’s another topic.

Global Exception Handling

One class handles all error responses consistently:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UrlNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleUrlNotFound(UrlNotFoundException ex) {
        // Return structured error response
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationErrors(...) {
        // Return validation errors
    }
}

Database Migrations with Flyway

Flyway integration is automatic. Just drop SQL files in a certain folder and we are done:

-- V1__initial_migration.sql
CREATE TABLE url
(
    id            BIGSERIAL PRIMARY KEY,
    path          TEXT NOT NULL,
    minified_path TEXT NOT NULL UNIQUE
);

-- V2__add_tracking_columns.sql
ALTER TABLE url
    ADD COLUMN click_count BIGINT NOT NULL DEFAULT 0;
ALTER TABLE url
    ADD COLUMN created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW();
CREATE INDEX idx_url_minified_path ON url (minified_path);

Spring Boot detects Flyway on the classpath and runs migrations automatically on startup. Version control for your database schema, with zero configuration.


Deployment: Docker All the Way

I decided to deploy with Docker so with my instance of coolify running, I can do automatic deploys on commit.


Conclusion

Building Tiniest for me shows the power of modern Java development:

  1. Spring Boot & modern Java eliminates boilerplate, letting you focus on business logic
  2. Docker makes deployment consistent across environments
  3. Compose profiles adapt the same codebase from development to production

The entire codebase is remarkably small—around 12 Java files, yet it’s production-ready.

Check Tiniest Out

You may like: