Shortify

URL shortening: a model, a workflow and a storage contract in PHP 8.3.

A small PHP library as a practical DDD case: URL validation, a shorten/expand facade, a repository boundary and explicit code-generation limits.

PHP · DDD · URL Shortener · Architecture

Role

Library author: URL model, shortening service, facade, repository and tests.

Open source · September 2026. This case examines public commit 779d5e59. Source links and implementation limits refer to that revision.

A small domain with a clear storage boundary

I wrote Shortify as a PHP library for two operations: producing a short code for a URL and finding the original URL by that code. It is a useful small-scale architecture exercise because validation, value generation, persistence and missing records all appear in a few classes. The design can be discussed through concrete code rather than a large diagram.

I am the author of the URL model, shortening service, facade, repository interface and tests. The package requires PHP ^8.3, as the Composer manifest confirms. Its require section contains no third-party runtime libraries. DDD here is expressed through named operations and explicit dependencies.

Creating a short code
  1. ShortifyThe shorten / expand facade
  2. CoreUrl, Shortener and CodeGenerator
  3. RepositoryStorage contract and selected adapter

An InMemoryRepository supports local experiments. The application supplies persistent storage and an HTTP redirect endpoint.

Workflow: shorten and expand one link

No database is needed to try the library. The bundled InMemoryRepository stores objects in an array inside the current process. The facade receives that repository, creates a URL with a short code and returns the original address when expand is called. This is a complete example after installing the package.

<?php
require __DIR__ . '/vendor/autoload.php';

use Tenqz\Shortify\Shortify;
use Tenqz\Shortify\Infrastructure\Repository\InMemoryRepository;

$shortify = new Shortify(new InMemoryRepository());
$original = 'https://example.com/articles/php-boundaries';
$url = $shortify->shorten($original);
$code = $url->getShortCode();
$expanded = $shortify->expand($code);

echo $code . PHP_EOL;
echo $expanded . PHP_EOL;
if ($expanded !== $original) {
    throw new RuntimeException('Round trip failed');
}

The first output line is a generated code, so the example does not prescribe its value. The second should equal the original URL; the code explicitly checks that round trip. The mapping disappears when the process ends. There is no public short URL yet: the application supplies the domain, route and redirect response.

Decision 1: validate the URL on construction

Url uses FILTER_VALIDATE_URL, permits only http and https, and requires a host. Invalid input raises InvalidUrlException before saving. This centralizes input validation for shortening. The object is mutable: its constructor sets the original address, and setShortCode assigns the short code.

Decision 2: depend on a repository contract

Shortener receives UrlRepositoryInterface with save, findByCode and exists. It contains no SQL and selects no database. An application adapter can persist the same objects while retaining the library operations. The interface lives in the Infrastructure namespace. The Core service depends on that contract, and the constructor receives a concrete adapter.

Decision 3: separate the operation from the entry point

The Shortify facade assembles a Shortener and a code generator. The service makes the sequence visible: create a Url, generate a code, assign it and save. The inverse operation retrieves a record and raises UrlNotFoundException when it is absent. The application maps that exception to HTTP 404 or another interface-specific response.

What exists and how it is checked

The result is a Composer package with a small entry API and replaceable storage. Its integration test contains the shorten → expand round trip using an in-memory repository, an invalid URL and a missing code. The service unit test checks repository and generator interactions. The Shortener implementation exposes the sequence those tests describe.

The result contract is explicit: a saved code resolves to the original URL, invalid input is rejected, and a missing code raises a separate exception. The integration check covers class collaboration within one process. Concurrent writes to persistent storage require a separate adapter check.

What a public service still needs

The generator defaults to six characters from an MD5 string built with the URL, time and random bytes. Its source alphabet contains letters and digits, but the resulting string is hexadecimal. A code is neither an access token nor a guaranteed unique identifier.

Shortener does not detect collisions or regenerate codes; it does not call exists during shortening. InMemoryRepository replaces a record when a key collides. Persistent deployment needs a unique index and an explicit conflict policy with bounded retries. Checking exists before save alone cannot prevent races between concurrent requests.

Link expiry, abuse controls, analytics and access management also remain outside the package. URL syntax validation does not assess the safety of the destination.

Try it

composer require tenqz/shortify

On PHP 8.3 or later, save the example beside vendor and run it. Then inspect the repository contract and in-memory adapter. The complete project is available in tenqz/shortify. Start extensions by defining storage and collision requirements.

GitHub

Let's make the complex understandable.

Architecture, engineering leadership, and AI in development — when the system is too important to simplify, and too expensive not to own.

LinkedIn Telegram Email