> For the complete documentation index, see [llms.txt](https://docs.digit.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.digit.org/health/design/architecture/low-level-design/registries/facility.md).

# Facility

## Overview

Facility is the **registry of physical places** in a health campaign — warehouses, health posts, clinics, storage points and distribution centres. Each facility row records what the place is, how much it can hold, and where it sits on the map.

It stores name, usage or type, permanence, storage capacity, and a shared **address** with GPS coordinates and a boundary or locality code.

In short: *"which places exist in this campaign, what are they for, and where are they?"*

Other services point to these places. Stock records movements **into and out of** a facility. Project links facilities to a campaign plan. Facility is the single source of truth they validate against.

### At a glance

* **Used by:** campaign setup, stock flows, project planning, field search, and dashboards.
* **Input:** facility details, storage capacity, address, GPS, and boundary/locality code.
* **Output:** validated facility records, search results, and downstream persistence/index events.
* **Source of truth:** the facility registry, persisted as `facility` and `address` rows in Postgres.

## Dependencies

* **idgen** — generates facility IDs. Address IDs are UUIDs generated locally.
* **boundary-service** — validates that each facility's locality or boundary code exists, so a facility cannot point at an unknown place.
* **health-services-common / -models** — shared clients, validators, query builders, and the `Facility` / `Address` POJOs.
* **Kafka** — carries the async create, update, and delete pipeline.
* **egov-persister** (deployed via the `configs/` repo) — writes the `facility` and `address` rows to Postgres from the `save-*`, `update-*`, and `delete-*` topics.
* **egov-indexer / transformer → Elasticsearch** — builds the dashboard read model (`facilityIndex-v1`) from the same events. The transformer also caches facilities in memory to enrich other events.
* **Redis** — supports the shared repository cache layer. Search-by-id checks cache first.
* **Postgres** — system of record for `facility` and shared `address` tables, with tenant-aware schema resolution.

## Data Model

Facility stores the business identity of a place. It carries the facility name, usage, permanence flag, and storage capacity. Each facility owns one address row through `addressId`. That address stores the GPS coordinates and the boundary or locality code. Updates use `rowVersion` for optimistic locking. Deletes are soft through `isDeleted`.

<figure><img src="https://2077406040-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FI0KFbSBTySIQ7TCOKnJF%2Fuploads%2F4761wmRIJm4eek6AE8cQ%2Fimage.png?alt=media&amp;token=3f0ec81f-d61e-49e8-9525-13f221a499c1" alt=""><figcaption></figcaption></figure>

## Business Flow

* **During campaign setup**, facilities are created. This is usually done in bulk. Each facility is tied to a boundary or locality, so it has a known place on the map.
* **During campaign runtime**, other services look up facilities. Stock checks the facility before recording a movement. The field app searches facilities by area. Project associates facilities with campaign tasks.
* **As things change**, a facility's details can be updated. That includes name, capacity, and address. A facility can also be soft-deleted when it is no longer in use.
* **For dashboards and enrichment**, facility events flow through the transformer and indexer into Elasticsearch. The transformer also keeps facilities in memory so other events can be enriched with facility names and locations.

### Key APIs / Entry Points

Base path `/facility/v1`.

The service supports single and bulk create, update, delete, and search.

<table><thead><tr><th width="267.88671875">Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td><code>POST /facility/v1/_create</code></td><td>Register one facility. Validation and enrichment are synchronous. Persistence is asynchronous.</td></tr><tr><td><code>POST /facility/v1/bulk/_create</code></td><td>Register many facilities at once through Kafka.</td></tr><tr><td><code>POST /facility/v1/_update</code>, <code>/bulk/_update</code></td><td>Correct a facility's details.</td></tr><tr><td><code>POST /facility/v1/_delete</code>, <code>/bulk/_delete</code></td><td>Soft-delete a facility.</td></tr><tr><td><code>POST /facility/v1/_search</code></td><td>Find facilities by <code>isPermanent</code>, <code>usage</code>, <code>storageCapacity</code>, <code>boundaryCode</code> (locality), id, plus <code>tenantId</code> / <code>limit</code> / <code>offset</code> / <code>lastChangedSince</code> / <code>includeDeleted</code> URL params.</td></tr></tbody></table>

**Kafka entry points (async).** Bulk requests land on `create-facility-bulk-topic`, `update-facility-bulk-topic`, and `delete-facility-bulk-topic`. The service's own consumer picks them up. Persisted results go out on `save-facility-topic`, `update-facility-topic`, and `delete-facility-topic` for the persister, indexer, and transformer.

**Swagger contract:**

{% embed url="<https://editor.swagger.io/?url=https://raw.githubusercontent.com/egovernments/health-campaign-services/master/docs/health-api-specs/contracts/registries/facility.yml>" %}

#### Kafka topics

<table><thead><tr><th width="256.66796875">Topic</th><th width="157.55078125">Dir</th><th>Purpose</th></tr></thead><tbody><tr><td><code>create-facility-bulk-topic</code></td><td>in</td><td>Bulk facility create requests</td></tr><tr><td><code>update-facility-bulk-topic</code></td><td>in</td><td>Bulk facility update requests</td></tr><tr><td><code>delete-facility-bulk-topic</code></td><td>in</td><td>Bulk facility delete requests</td></tr><tr><td><code>save-facility-topic</code></td><td>out</td><td>Persist new facilities</td></tr><tr><td><code>update-facility-topic</code></td><td>out</td><td>Persist facility updates</td></tr><tr><td><code>delete-facility-topic</code></td><td>out</td><td>Persist facility soft-deletes</td></tr></tbody></table>

## Process Flow

Use this reading order:

1. **Send the request.** A client calls to create, update, or delete on `/facility/v1`.
2. **Validate and enrich.** The service checks the payload, generates IDs where needed, and validates the boundary or locality code.
3. **Publish the event.** The service acknowledges the request and publishes a Kafka event for persistence.
4. **Persist and index.** `egov-persister` writes the `facility` and `address` rows. The transformer and indexer update dashboard-facing read models from the same event.
5. **Serve reads.** Search reads from Postgres. Search-by-id checks Redis first.

Writes are **asynchronous**.

Reads are **synchronous**.

The service does not write to Postgres directly.

A `202` means the request was accepted, not that the rows are already visible in Postgres.

#### Sequence Diagrams

{% tabs %}
{% tab title="Facility - Create" %}

<figure><img src="https://2077406040-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FI0KFbSBTySIQ7TCOKnJF%2Fuploads%2FpoTIi3axq07eYsQEnAYO%2Fimage.png?alt=media&amp;token=a49f7b97-c731-4a88-a10c-d2bc175f1701" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="Facility - Update" %}

<figure><img src="https://2077406040-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FI0KFbSBTySIQ7TCOKnJF%2Fuploads%2F8Tirio01mM0AkJGiLFNs%2Fimage.png?alt=media&amp;token=e0a9598e-a85b-4d4a-9fdd-7a671833b63d" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="Facility - Search" %}

<figure><img src="https://2077406040-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FI0KFbSBTySIQ7TCOKnJF%2Fuploads%2FHlUlH163tkPteLVcX1oG%2Fimage.png?alt=media&amp;token=81ba282f-e8e1-4f73-9dcd-ae40cd5f3ac4" alt=""><figcaption></figcaption></figure>
{% endtab %}
{% endtabs %}

## Failure / Retry Handling

* **Async, no batch rollback.** A bulk request returns `202` before persistence. If one record in the batch fails validation in the consumer, it does not roll back the others. Check consumer logs and the record status. The consumer catches its own errors and logs them rather than re-throwing.
* **Boundary check is a hard stop.** If a facility's locality code is unknown, that record is flagged `NON_EXISTENT_ENTITY`. If the boundary-service call itself fails, the whole batch errors with `BOUNDARY_SERVICE_SEARCH_ERROR`.
* **Idempotency** is via `clientReferenceId`. Resubmitting the same one should not create a duplicate row.
* **Optimistic locking** via `rowVersion` protects against concurrent edits on update.
* **Soft delete** (`isDeleted`) applies everywhere. Nothing is hard-deleted. Search excludes deleted rows by default unless `includeDeleted=true`.
* If the **persister config** for the facility topics is missing or stale in an environment, the API can accept writes while rows never appear in Postgres.

{% hint style="info" %}

### Recent Changes&#x20;

* **Maintenance-only release**: No new APIs, search filters, columns, or business behaviour changes. Updates are limited to `pom.xml`, `application.properties`, `CHANGELOG.md`, and a test config.
* **Tracer / error-handling upgrade**: Bumped shared `health-services-common` (carrying tracer `2.9.2`), so database errors are now handled uniformly via tracer's `ExceptionAdvise` — clients get a consistent error shape instead of a raw 500 on a `DataAccessException`.
* **OpenTelemetry plumbing added**: OpenTelemetry BOM and instrumentation BOM added to `pom.xml` for version alignment. OTEL exporters (`otel.traces/logs/metrics.exporter`) are explicitly set to `none`, keeping tracing opt-in per environment rather than on by default.
* **Shared-library version bumps**: `health-services-common` and `health-services-models` updated in lockstep across health services; facility module version moved to 1.2.1.
  {% endhint %}

## Known Risks / Limitations

* **Search filters are basic.** Only `isPermanent`, `usage`, `storageCapacity` and `boundaryCode` are real query columns. There is no name search, no project filter, and no "updated by others" sync filter on this branch.
* **`usage` is a free string.** Conventions are valued like a warehouse or health post, but the DB does not enforce them.
* **Boundary-service is a hard dependency for create and update.** A boundary lookup failure aborts the batch. An environment with a misconfigured or down boundary service cannot register facilities.
* **Address is denormalised per facility.** Each facility owns its own address row through `addressId`. The same physical place created twice yields two address rows.
* **Search-by-id reads Redis first.** Stale cache entries can, in theory, be returned for ID lookups. Filtered non-ID search always hits Postgres.
* **No batch rollback.** Partial success in a bulk request is normal. Downstream consumers must reconcile from per-record status, not from the `202`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.digit.org/health/design/architecture/low-level-design/registries/facility.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
