DEV Community

Ilya Mikhasik
Ilya Mikhasik

Posted on

Our System Series: Registries. From the Network Discovery Service to Reusable Registries

Registries entities

Registries links

Previous: User Signup Service

In the previous article, I described user signup as an example of how our services interact. This article looks at the registry layer in more detail: where the idea came from, what a registry represents, and how application services communicate with one.

The Network Discovery Service

The idea of registries originated in an application I will call the Network Discovery Service, internally named Kroozheva.

The application was designed to represent networks of connected objects. Users could create objects such as people, workplaces, cities, and professions, then connect them to one another. These relationships could be used to discover new contacts through existing connections.

For example, a user could create themselves as a node, add family members, friends, workplaces, and cities, and connect these objects. They could then search the network for someone with a particular profession who was connected through a friend or another trusted path.

This requirement influenced the database design from the beginning.

A model based on fixed entity types would require separate tables for people, places, professions, and every other object type. Relationships would require additional intermediate tables, and the number of tables would grow as new entity types were introduced.

Instead, we designed a generic model based on entities and links:
Entity ─── Link ─── Entity

An entity contains common fields such as its identifier, type, dates, status, and flexible JSON data. A link represents a relationship between two entities and contains the connected identifiers, direction, type, weight, and any relationship-specific data.

This gave us a flexible data model for representing networks without creating a new relationship structure for every combination of entity types.

From the data model to registries

Once the generic entity model had been defined, we needed a consistent way to access the stored objects.

Every new entity type required the same basic operations:

  • Create records
  • Retrieve individual records
  • Search and list records
  • Update records
  • Delete or deactivate records
  • Perform batch operations where necessary

Writing these operations repeatedly for every entity would have produced a large amount of nearly identical code. It would also have made behavior inconsistent between services.

This led to the idea of registries: database-access services with a standard CRUD interface for a particular registry or table.

A registry could represent a conventional entity, such as users or profiles, or a relationship structure such as links. The application layer could then use the same style of interface regardless of which registry it needed to access.

Registry API

A typical registry exposes separate endpoints for individual and collection operations. The attached Swagger UI shows this pattern using good, goods, and link registries.

This convention makes registry APIs predictable. A service that already knows how to interact with one registry can use the same general approach with another.

Querying registry collections

A registry is more useful than a basic CRUD interface because its collection endpoints support structured queries.

A request can limit the number of returned records and specify the starting position:
GET /api/objects/?limit=10
GET /api/objects/?limit=10&offset=5

The first request returns ten records. The second starts at offset five and returns the next page.

The API also supports filtering by standard fields:
`GET /api/objects/?object_type=person
GET /api/objects/?project_id=3fa85f64-5717-4562-b3fc-2c963f66afa6
GET /api/objects/?account_id=3fa85f64-5717-4562-b3fc-2c963f66afa6
GET /api/objects/?created_date__gt=2023-06-15'

Several filters can be combined:
'GET /api/objects/?limit=10&offset=5&object_type=person&name=Ivan'

This returns one page of objects matching both the object type and name.

The same query conventions can be used across different registries, so application services do not need a separate querying approach for every entity type.

Querying flexible data

The generic model includes a JSON data field for entity-specific attributes. The registry API supports filtering inside this field without requiring every possible attribute to become a database column.

Examples include:
GET /api/objects/?data=username__exact=Ivan
GET /api/objects/?data=number__gte=18::int
GET /api/objects/?data=number__lt=12.5::float

The query identifies the JSON key, the lookup operation, and, when necessary, the expected value type.

Supported lookups include:
icontains
exact
iexact
gt
gte
lt
lte
endswith
iendswith
startswith
istartswith

Several JSON-field filters can be combined:
GET /api/objects/?data=username__icontains=a&data=some_id__exact=23

This capability is important for the generic registry model. Entity-specific information can be stored and queried while the registry retains a common structure.

Searching and ordering

The API also supports general search.

Without a field restriction, the search value is applied across all searchable fields:
GET /api/objects/?search=2

A search can be limited to one field:
GET /api/objects/?search=2&field=object_code

It can also target several fields:
GET /api/objects/?search=350&field=object_code&field=project_id&field=data

Nested JSON fields can be addressed with double-underscore notation:
GET /api/objects/?search=+78564523636&field=data__telephone

Results can be ordered explicitly:
GET /api/objects/?ordering=object_code
GET /api/objects/?ordering=-object_code

Multiple ordering fields are supported:
GET /api/objects/?ordering=object_code,created_date

Ordering can also use values inside the JSON data field:
GET /api/objects/?ordering=data__number,-data__date

The default ordering is meta__internal_id in descending order, so newer objects appear first unless another ordering is requested.

Pagination, filtering, searching, and ordering make the registry API useful for both ordinary application requests and larger data-processing operations.

The registry interaction module

Application services do not construct registry requests independently. We use a shared module that centralizes communication with registry services.

Its main entry point is:
async def interact_with_registry(...)

The function receives:

  • The incoming FastAPI request
  • The HTTP method
  • The registry base URL
  • The registry name
  • An optional object ID
  • Query parameters
  • Request data
  • Filtering and error-handling options
  • A request timeout

It constructs the registry URL from the supplied registry name and optional ID, serializes request data, forwards the session context, performs the HTTP request, validates the response, and converts registry failures into application-specific exceptions.

This keeps registry interaction consistent across application services.

Request construction

The module supports the standard HTTP methods:
GET
POST
PUT
PATCH
DELETE

When an object ID is not supplied, the module builds a collection URL:
{registry_url}/{registry_name}/

When an ID is supplied, it builds an individual-object URL:
'{registry_url}/{registry_name}/{id}/'

Request bodies are serialized with FastAPI’s jsonable_encoder, which allows schema objects and values such as UUIDs and datetimes to be converted into JSON-compatible data before being sent to the registry.

Timeouts and failures

The module obtains the HTTP client from the FastAPI application and applies a configurable timeout to every request.

Transport-level failures are logged and converted into RegistryInteractionException. This gives application services a consistent error type instead of exposing the underlying HTTP client’s exception classes.

The response body is parsed as JSON. If parsing fails, the module raises IncorrectFormatInRegistryException

The module also handles several registry-level conditions:

  • A 404 response or empty response can become NotFoundException
  • A duplicate-object response containing exists becomes EntityExistsException
  • A hidden or deactivated object can become RegistryObjectDeactivatedException
  • Other unsuccessful responses become RegistryInteractionException

The active_records and raise_not_found parameters allow a caller to adjust this behavior for a particular operation.

The shared module therefore acts as an integration boundary. Registry-specific HTTP behavior is handled in one place, while application services work with consistent application-level exceptions.

Creating a registry

The Registry Factory was created to reduce the repetitive work involved in introducing new registries.

A developer creates a registry by running a Django management command:
python manage.py createregistry --template=registry_template.zip people --model_name=Person

The command receives:

  • The template to use
  • The registry name, such as people
  • The model name, such as Person

The factory generates the application structure for the new registry.

The generated registry is then added to INSTALLED_APPS:
INSTALLED_APPS = [
"registry_config",
"links",
"rest_framework",
"django_filters",
"people",
]

The developer creates and applies database migrations:
python manage.py makemigrations
python manage.py migrate

The generated viewsets are then registered with the project router:
router.register(r"people", PeopleViewset)
router.register(r"person", PersonViewset)

This exposes collection and individual-object endpoints using the same convention as existing registries.

Testing and documentation

The registry can be tested independently:
python manage.py test people

A coverage report can be generated with:
coverage run manage.py test people

The service can then be started locally:
python manage.py runserver

Its API can be inspected through Swagger UI.

The project wiki documents this workflow, including registry creation, project integration, migrations, testing, and API access.

The documentation serves two audiences:

  • Developers creating a new registry
  • Developers integrating with an existing registry

This distinction is important. Someone using a registry should be able to understand its API without studying the factory’s implementation. The generated service should be predictable, and its documentation should make its behavior discoverable.

From command to working API

The complete workflow is:
Run the factory command
→ add the generated registry to the project
→ create and apply migrations
→ register the viewsets
→ run tests
→ inspect the API in Swagger UI

This is what turns the Registry Factory from a code-generation script into a practical development tool. It does not merely create files; it defines a repeatable path from a new entity name to a tested, documented, database-backed API.

The factory provides the common structure. Developers can then add entity-specific fields, validation, permissions, and business rules where necessary.

Generic persistence and business logic

Registries are intentionally generic. They manage records and expose persistence operations, but they do not own the complete business meaning of a workflow.

The Registry Factory defines the common structure of a registry and its entities. Developers are not expected to redesign that structure for every new registry. The purpose of the factory is to ensure that registries follow the same data model, API conventions, metadata structure, filtering behavior, and CRUD interface.

This standardization allows application services to work with different registries through the same interaction patterns.

In the next article, I will describe how our system encrypts protected data before storing it in the database. I will explain where encryption takes place in the service architecture, how encrypted data is passed to the registry layer, and how the design separates data protection from generic persistence operations.

Top comments (0)