# 0004. Static repository services with per-request memoization

- **Status:** Accepted
- **Date:** 2025-11-11

## Context

Reference data — areas, locations, storage types, contents, FAQs — is read many times per page: by the
controller, by three widgets, by the footer. Each read was a query, and the same query several times per
request.

## Decision

Put every repeated read behind a **static service class** in `services/`, memoizing the result in a `static`
local for the life of the process, and layering Yii's query cache underneath:

```php
class AreaService {
    public static function published() : array {
        static $published;
        $published ??= static::query()->all();
        return $published;
    }
    private static function query() : ActiveQuery {
        static $query;
        $query ??= Area::find()->where( [ 'isPublished' => true ] )->cache();
        return $query;
    }
}
```

## Alternatives

- **Instance services registered as application components.** More ceremony, and the container would have to
  be threaded through widgets that are constructed by the view.
- **Query cache only.** Still a cache round-trip per call; with several widgets per page that is measurable.
- **Yii's `ActiveRecord` cache dependency machinery.** More configuration than the problem needs, and
  invalidation here is flush-all anyway — see [0020](0020-cache-invalidation-is-flush-all.md).

## Consequences

- **Repeated reads are free after the first.** No cache backend involved on the second call.
- **A `static` inside a static method lives for the whole PHP process.** Correct in a web request; in a
  console command that mutates data and reads it back through a service, the first result is frozen for the
  entire run. This is the sharpest edge of the decision.
- No dependency injection and no way to substitute a service in a test — acceptable, because there are no
  automated tests.
- The directory ended up holding two unrelated kinds of class: these static repositories, and the
  **instantiated integration clients** (`TellGateService`, `AiTranslationService`). Check the base class
  before assuming an API.
- `StorageService::usage()` follows the pattern to its logical end: it memoizes the occupancy map for
  *every* storage on the first call for *any* storage, because the calendar needs all of them anyway.
