<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ilya Mikhasik</title>
    <description>The latest articles on DEV Community by Ilya Mikhasik (@ilya_mikhasik_f8c2953d624).</description>
    <link>https://dev.to/ilya_mikhasik_f8c2953d624</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4131481%2F0798c2a9-665b-42e0-8c09-22fc6311d58c.jpg</url>
      <title>DEV Community: Ilya Mikhasik</title>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ilya_mikhasik_f8c2953d624"/>
    <language>en</language>
    <item>
      <title>Our System Series: Cerberus. Permissions and Endpoint Access</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Fri, 25 Sep 2026 12:10:34 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-cerberus-permissions-and-endpoint-access-3d5k</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-cerberus-permissions-and-endpoint-access-3d5k</guid>
      <description>&lt;p&gt;Previous: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-jwt-tokens-identity-context-and-permissions-3266"&gt;JWT Tokens&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a user signs in, the system needs more than an answer to the question, “Who is this?” It also needs to answer, “What is this user allowed to do in this project?”&lt;/p&gt;

&lt;p&gt;Cerberus is the platform’s centralized permissions service. It manages which users can access which operations in which services, and it manages groups that can be used as part of the same access model.&lt;/p&gt;

&lt;p&gt;The User Profiles Service retrieves this information during login and includes it in the JWT. Services then use the shared authentication library to decide whether a request can reach a particular endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions
&lt;/h2&gt;

&lt;p&gt;A user does not simply have a generic “administrator” or “editor” label. Permissions are assigned for specific operations in specific services.&lt;/p&gt;

&lt;p&gt;For example, a user may be allowed to manage groups in Cerberus while also being allowed to create tasks in the Quest service:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{&lt;br&gt;
  "Cerberus": [&lt;br&gt;
    "post_group_group_name_users",&lt;br&gt;
    "delete_group_group_name_user_user_id"&lt;br&gt;
  ],&lt;br&gt;
  "Quest": [&lt;br&gt;
    "get_quests",&lt;br&gt;
    "patch_quest_quest_id",&lt;br&gt;
    "post_task"&lt;br&gt;
  ]&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This expresses access in terms of the capabilities offered by each service.&lt;/p&gt;

&lt;p&gt;Cerberus keeps a configured list of recognized services. Each service name is mapped to a UUID in the service configuration. When permissions are assigned, Cerberus verifies that the target service is part of this known set.&lt;/p&gt;

&lt;p&gt;This prevents permission records from being created for arbitrary or misspelled service names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing access
&lt;/h2&gt;

&lt;p&gt;Cerberus provides endpoints for viewing and changing a user’s permissions:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET    /permissions/user/{user_id}&lt;br&gt;
POST   /permissions/user/{user_id}&lt;br&gt;
DELETE /permissions/user/{user_id}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When assigning permissions, the request identifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user receiving access&lt;/li&gt;
&lt;li&gt;The project where the access applies&lt;/li&gt;
&lt;li&gt;The target service&lt;/li&gt;
&lt;li&gt;The operations that should be allowed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Permissions are assigned within a project. When a caller changes another user’s permissions, Cerberus verifies that the request’s Project-ID matches the project carried in the caller’s token.&lt;/p&gt;

&lt;p&gt;Permission management is itself protected. A user must have the appropriate Cerberus permission before they can grant, change, or remove another user’s access.&lt;/p&gt;

&lt;p&gt;The service also supports default permissions for a project. These make it possible to establish a baseline level of access and then add more specific permissions for individual users when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Groups
&lt;/h2&gt;

&lt;p&gt;Permissions are not always managed only person by person. Cerberus also supports groups.&lt;/p&gt;

&lt;p&gt;A group is an entity, and membership is represented through the same link-based model used elsewhere in the system:&lt;br&gt;
User ─── user_group link ─── Group&lt;/p&gt;

&lt;p&gt;Cerberus provides operations for retrieving a user’s groups, adding users to a group, and removing users from a group.&lt;/p&gt;

&lt;p&gt;The User Profiles Service retrieves group membership during token creation. The group information then travels with the rest of the user’s access context.&lt;/p&gt;

&lt;p&gt;This allows services to use both direct permissions and group membership when applying access rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared authentication library
&lt;/h2&gt;

&lt;p&gt;Cerberus manages permission assignments, but it does not sit in front of every request to every service.&lt;/p&gt;

&lt;p&gt;Each service uses a shared authentication library. The library provides the validate_permissions decorator, which is applied to endpoints that need access control:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@validate_permissions("POST_PERMISSIONS_USER_USER_ID")&lt;br&gt;
async def set_user_permissions_in_project(...):&lt;br&gt;
    ...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The decorator executes before the endpoint function. It reads the service’s authorization configuration, extracts and checks a token when necessary, verifies project context, and allows or rejects the request before business logic runs.&lt;/p&gt;

&lt;p&gt;This gives services a common enforcement mechanism while allowing each service to define its own endpoint permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint configuration
&lt;/h2&gt;

&lt;p&gt;Each service keeps its endpoint access rules in auth.yml.&lt;/p&gt;

&lt;p&gt;For example, Cerberus defines:&lt;br&gt;
`debug_mode: PROD&lt;/p&gt;

&lt;p&gt;permissions:&lt;br&gt;
  INFO: OPEN&lt;/p&gt;

&lt;p&gt;GET_PERMISSIONS_USER_USER_ID: OPEN&lt;br&gt;
  POST_PERMISSIONS_USER_USER_ID: GRANTED&lt;br&gt;
  DELETE_PERMISSIONS_USER_USER_ID: GRANTED&lt;/p&gt;

&lt;p&gt;GET_GROUPS_USER_USER_ID: OPEN&lt;br&gt;
  POST_GROUP_GROUP_NAME_USERS: GRANTED&lt;br&gt;
  DELETE_GROUP_GROUP_NAME_USER_USER_ID: GRANTED`&lt;/p&gt;

&lt;p&gt;The endpoint code identifies the operation:&lt;br&gt;
&lt;code&gt;@validate_permissions("POST_GROUP_GROUP_NAME_USERS")&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This keeps endpoint business logic separate from access policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access levels
&lt;/h2&gt;

&lt;p&gt;The library recognizes three access levels:&lt;/p&gt;

&lt;p&gt;OPEN -- No token is required&lt;br&gt;
AUTHORIZED -- A valid JWT is required&lt;br&gt;
GRANTED -- A valid JWT containing the required permission is required&lt;/p&gt;

&lt;p&gt;An OPEN endpoint is available to any caller.&lt;/p&gt;

&lt;p&gt;An AUTHORIZED endpoint requires a Bearer token that can be validated with the service’s JWT public key and configured algorithm.&lt;/p&gt;

&lt;p&gt;A GRANTED endpoint requires a valid token and the permission associated with the endpoint. The permission can be assigned directly to the user or made available through group membership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request validation
&lt;/h2&gt;

&lt;p&gt;The shared decorator applies the following flow:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Request&lt;br&gt;
  → validate_permissions&lt;br&gt;
    → resolve endpoint access level from auth.yml&lt;br&gt;
      → extract token when required&lt;br&gt;
        → read user context from the token&lt;br&gt;
          → compare token project with request Project-ID&lt;br&gt;
            → validate token or required permission&lt;br&gt;
              → execute endpoint&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Development and production behavior
&lt;/h2&gt;

&lt;p&gt;The configuration includes a debug mode.&lt;/p&gt;

&lt;p&gt;In development mode, an endpoint that is absent from auth.yml is treated as open. In production mode, an endpoint without an access configuration is denied.&lt;/p&gt;

&lt;p&gt;This makes production behavior fail closed: an endpoint must be intentionally configured before it can be exposed.&lt;/p&gt;

&lt;p&gt;Authorization can also be disabled entirely through service configuration. In that case, the decorator becomes a no-op and returns the original function without applying access checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing perspective
&lt;/h2&gt;

&lt;p&gt;Cerberus provides the centralized model for permissions and groups. The shared authentication library turns that model into endpoint protection across services.&lt;/p&gt;

&lt;p&gt;Cerberus determines which operations a user may perform. The User Profiles Service places that resolved context into a JWT. Each service uses auth.yml to classify its endpoints as open, authenticated, or permission-restricted, and validate_permissions enforces that classification before business logic runs.&lt;/p&gt;

&lt;p&gt;This gives the platform one permission model and one access-validation mechanism while allowing each service to define the rules appropriate to its own API.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Our System Series: JWT Tokens. Identity, Context, and Permissions</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Thu, 24 Sep 2026 13:39:30 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-jwt-tokens-identity-context-and-permissions-3266</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-jwt-tokens-identity-context-and-permissions-3266</guid>
      <description>&lt;p&gt;Previous: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-event-processing-from-business-events-to-external-actions-4d9p"&gt;Event Processing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every request in a microservice system needs context. A service needs to know who is making the request, which project the request belongs to, which account is being used, and what the caller is allowed to do.&lt;/p&gt;

&lt;p&gt;In our system, this context is carried by JWT access tokens.&lt;/p&gt;

&lt;p&gt;A token is created after a successful login and then passed to the services involved in the request. It contains the user identity together with the project, account, organization, group, and permission context required for authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Login is more than password verification
&lt;/h2&gt;

&lt;p&gt;The User Profiles Service exposes two endpoints for obtaining an access token:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /auth/login&lt;br&gt;
POST /auth/token&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Both endpoints authenticate a user with a username and password, require a Project-ID header, and can receive an Account-ID header.&lt;/p&gt;

&lt;p&gt;POST /auth/login returns a full login response. It includes the access token together with selected user data, abbreviated profile data, organization information, permissions, and groups.&lt;/p&gt;

&lt;p&gt;POST /auth/token returns only the access token and token type. It is intended for callers that already have the user context they need and only require a new Bearer token.&lt;/p&gt;

&lt;p&gt;The authentication process begins by locating the active user by login and verifying the password. Before password verification, the service checks account flags that may require the user to restore or change their password.&lt;/p&gt;

&lt;p&gt;But a successful password check is only the beginning.&lt;/p&gt;

&lt;p&gt;The service must also determine where the user is acting in the system. The same user may have access to different projects, accounts, organizations, groups, and operations. A useful access token therefore needs to carry more than a user ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the token contains
&lt;/h2&gt;

&lt;p&gt;The access token is a signed JWT containing a sub object. This object represents the security context created during login.&lt;/p&gt;

&lt;p&gt;The basic identity and context fields are:&lt;br&gt;
user_id&lt;br&gt;
profile_id&lt;br&gt;
project_id&lt;br&gt;
account_id&lt;br&gt;
device_id&lt;br&gt;
user_ip&lt;/p&gt;

&lt;p&gt;Depending on the user’s relationships and assigned access, the token also contains:&lt;br&gt;
permissions&lt;br&gt;
groups&lt;br&gt;
organizations&lt;/p&gt;

&lt;p&gt;A simplified token payload looks like this:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
  "sub": {&lt;br&gt;
    "user_id": "898ccff4-ba19-4a52-a3e3-4b631047e6a9",&lt;br&gt;
    "profile_id": "898ccff4-ba19-4a52-a3e3-4b631047e6a9",&lt;br&gt;
    "project_id": "2c03471b-7792-4f9a-aa8a-6811810959f0",&lt;br&gt;
    "account_id": "9fe20687-4036-48ba-951d-458092012288",&lt;br&gt;
    "permissions": {&lt;br&gt;
      "Quest": [&lt;br&gt;
        "get_quests",&lt;br&gt;
        "post_quest"&lt;br&gt;
      ]&lt;br&gt;
    },&lt;br&gt;
    "groups": [&lt;br&gt;
      "organization"&lt;br&gt;
    ],&lt;br&gt;
    "organizations": [&lt;br&gt;
      "f607b439-d88c-41c8-8581-5655f71722cf"&lt;br&gt;
    ]&lt;br&gt;
  },&lt;br&gt;
  "iat": 1790254220,&lt;br&gt;
  "exp": 1790297420&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The API response also returns selected user and profile details. For example, the caller receives user information, account flags, an abbreviated profile, and the Bearer token itself.&lt;/p&gt;

&lt;p&gt;The token is what subsequent services use to identify the caller and evaluate access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the context
&lt;/h2&gt;

&lt;p&gt;The token’s project and account information is not simply copied from request headers. The User Profiles Service resolves the user’s relationships before placing that information into the token.&lt;/p&gt;

&lt;p&gt;First, the service verifies that the user is connected to the current project through a project-user link.&lt;/p&gt;

&lt;p&gt;It then looks for account-user links within that project. If there is one matching account, the service uses it. If several accounts are available, the caller specifies the intended account through the Account-ID header.&lt;/p&gt;

&lt;p&gt;The service then verifies that the selected account is connected to the project through a project-account link. Finally, it retrieves the organizations to which the user belongs within that project and account.&lt;/p&gt;

&lt;p&gt;User&lt;br&gt;
  → project-user link&lt;br&gt;
    → account-user link&lt;br&gt;
      → project-account link&lt;br&gt;
        → organization memberships&lt;br&gt;
          → token context&lt;/p&gt;

&lt;p&gt;This is where the graph-like model becomes part of authorization. The links that connect users, projects, accounts, and organizations are used to construct the context in which a user can act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions and groups
&lt;/h2&gt;

&lt;p&gt;Permissions are retrieved from the permissions service and added to the JWT under the relevant service name.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
  "permissions": {&lt;br&gt;
    "FletcherGoods": [&lt;br&gt;
      "post_goods",&lt;br&gt;
      "patch_goods_goods_id",&lt;br&gt;
      "delete_goods_goods_id",&lt;br&gt;
      "patch_goods_goods_id_status"&lt;br&gt;
    ],&lt;br&gt;
    "Quest": [&lt;br&gt;
      "get_quests",&lt;br&gt;
      "patch_quest_quest_id",&lt;br&gt;
      "post_task"&lt;br&gt;
    ]&lt;br&gt;
  }&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The structure provides a direct mapping between a service and the operations permitted for that user.&lt;/p&gt;

&lt;p&gt;Groups provide another part of the authorization context. A user may belong to one or more groups within a project, and those groups are included in the token alongside individual permissions.&lt;/p&gt;

&lt;p&gt;The /auth/login endpoint can also receive a requested permission set. If it does, the service filters that request against the user’s actual permissions. The resulting token contains only permissions already assigned to that user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing tokens
&lt;/h2&gt;

&lt;p&gt;Tokens use RS256, an RSA signature algorithm based on SHA-256. The private key signs tokens, while services use the corresponding public key to verify them.&lt;/p&gt;

&lt;p&gt;Toknes also contain two time-based claims:&lt;br&gt;
iat — issued at&lt;br&gt;
exp — expiration time&lt;/p&gt;

&lt;p&gt;These record when a token was created and when it stops being accepted.&lt;/p&gt;

&lt;p&gt;After a token has been issued, protected endpoints use FastAPI’s OAuth2PasswordBearer dependency to extract the Bearer token from the request. The service verifies it with the public key, reads the user ID from sub, and loads the current user record from the registry.&lt;/p&gt;

&lt;p&gt;Invalid, expired, malformed, or userless tokens become application-specific authorization errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different lifetimes for different flows
&lt;/h2&gt;

&lt;p&gt;The normal access-token lifetime is controlled through configuration. The service also supports two scope-based lifetime modes:&lt;br&gt;
short:&lt;br&gt;
infinite:&lt;/p&gt;

&lt;p&gt;Short tokens default to five minutes if no explicit duration is supplied. They are used for focused actions such as password restoration.&lt;/p&gt;

&lt;p&gt;For example, a password-restore token contains a reduced security context:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
  "user_id": "&amp;lt;user UUID&amp;gt;",&lt;br&gt;
  "project_id": "&amp;lt;project UUID&amp;gt;",&lt;br&gt;
  "permissions": {&lt;br&gt;
    "UserProfiles": [&lt;br&gt;
      "patch_auth_password_trusted"&lt;br&gt;
    ]&lt;br&gt;
  }&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The trusted password-change endpoint validates the token’s remaining lifetime before allowing the password update.&lt;/p&gt;

&lt;p&gt;This gives the password-recovery flow a token with a specific purpose and a short usable period, rather than requiring the full set of permissions carried by a normal access token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing perspective
&lt;/h2&gt;

&lt;p&gt;JWTs connect authentication to the rest of the system.&lt;/p&gt;

&lt;p&gt;A password verifies the user’s identity at login. The user’s links to projects, accounts, and organizations establish the context in which that identity is operating. Groups and permissions determine which operations are allowed. The resulting JWT carries that context to the services handling later requests.&lt;/p&gt;

&lt;p&gt;In the next article, I will describe Cerberus, the permissions service. I will look more closely at how authentication, authorization, and permission checks are applied by services after a token has been issued.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-cerberus-permissions-and-endpoint-access-3d5k"&gt;Permissions and Endpoint Access&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Our System Series: Event Processing. From Business Events to External Actions</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Wed, 23 Sep 2026 13:59:37 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-event-processing-from-business-events-to-external-actions-4d9p</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-event-processing-from-business-events-to-external-actions-4d9p</guid>
      <description>&lt;p&gt;Previous: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-sensitive-data-encryption-36m5"&gt;Sensitive Data Encryption&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Signup Service creates the user and profile records, and the User Profiles Service encrypts protected data before it is stored. There is still one task left after successful registration: notifying the user.&lt;/p&gt;

&lt;p&gt;That work is handled by the Event Processor.&lt;/p&gt;

&lt;p&gt;The Event Processor provides a controlled way for services to publish events, route those events through configurable handlers, and deliver the resulting messages through external channels such as email, REST APIs and messengers.&lt;/p&gt;

&lt;p&gt;The signup email is one example. The same mechanism can support many kinds of follow-up work without placing that work directly inside the service that created the original event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use an Event Processor?
&lt;/h2&gt;

&lt;p&gt;A business workflow should not need to perform every follow-up operation synchronously.&lt;/p&gt;

&lt;p&gt;After a user signs up, the Signup Service needs to return the new user and profile identifiers. It should not also need to know how to select an email template, configure an email provider, send the email, handle delivery failure, or retry a temporary error.&lt;/p&gt;

&lt;p&gt;Instead, it publishes a signup event.&lt;/p&gt;

&lt;p&gt;The service that produces an event and the service that consumes it remain independent. Kafka topics provide the boundary between them: producers publish events to a topic, while consumers subscribe to and process events from that topic. Kafka documentation describes producers and consumers as decoupled, with events organized and durably stored in topics.&lt;/p&gt;

&lt;p&gt;Event ingress&lt;br&gt;
The Event API receives events through:&lt;br&gt;
&lt;code&gt;POST /event/send/{event_topic}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The endpoint validates the caller’s permissions, verifies that the requested topic is allowed, adds service-level context, and publishes the event to Kafka.&lt;/p&gt;

&lt;p&gt;Each event message contains:&lt;br&gt;
event_id&lt;br&gt;
project_id&lt;br&gt;
data&lt;br&gt;
token, when provided&lt;/p&gt;

&lt;p&gt;The Event API generates a UUID event_id for every incoming event. This provides a stable identifier that can be used to trace the event through processing stages and logs.&lt;/p&gt;

&lt;p&gt;The project_id identifies the project context. When the caller supplies an authorization token, the Event API includes it in the Kafka message so downstream actions can use the required authorization context.&lt;/p&gt;

&lt;p&gt;The API prevents callers from publishing directly to internal service topics. It also verifies that the requested Kafka topic exists. If the requested topic is missing from the cached topic list, the service refreshes the list before returning a not-found error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Kafka producer
&lt;/h2&gt;

&lt;p&gt;The Event API creates one shared AIOKafkaProducer during the FastAPI application lifespan.&lt;/p&gt;

&lt;p&gt;At startup, the service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Initializes its shared HTT P client&lt;/li&gt;
&lt;li&gt;Initializes logging and authorization&lt;/li&gt;
&lt;li&gt;Loads the available Kafka topics&lt;/li&gt;
&lt;li&gt;Creates and starts a Kafka producer using the configured security settings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;FastAPI lifespan handlers are designed for logic that runs before the application begins serving requests and during shutdown, which makes them suitable for managing shared resources such as external clients and message producers. AIOKafkaProducer.send_and_wait() publishes a record and waits for the send operation to complete.&lt;/p&gt;

&lt;p&gt;Using one producer for the application avoids creating and stopping a separate Kafka producer for every incoming event.&lt;/p&gt;

&lt;h2&gt;
  
  
  EVENT, PREPARE, and PUSH
&lt;/h2&gt;

&lt;p&gt;The intended processing model has three phases:&lt;/p&gt;

&lt;p&gt;EVENT → PREPARE → PUSH&lt;/p&gt;

&lt;h3&gt;
  
  
  EVENT
&lt;/h3&gt;

&lt;p&gt;The Event API receives an event from another service, validates it, enriches it with event and project context, and publishes it to Kafka.&lt;/p&gt;

&lt;p&gt;The Event API does not need to know whether the event will lead to an email, an external REST call, a messenger notification, or another action. It only accepts and publishes the event.&lt;/p&gt;

&lt;h3&gt;
  
  
  PREPARE
&lt;/h3&gt;

&lt;p&gt;Some events need transformation before they can be delivered. An email must be rendered from a template, a REST endpoint may require a particular JSON structure, and a messenger text may require its own formatting.&lt;/p&gt;

&lt;p&gt;A configured processor consumes an event from one topic, runs one or more preparation actions, and publishes the resulting delivery packages to the next topic.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
  "topic": "accrual-send",&lt;br&gt;
  "actions": [&lt;br&gt;
    {&lt;br&gt;
      "module": "prepare_json",&lt;br&gt;
      "next_topic": "RestAPI"&lt;br&gt;
    }&lt;br&gt;
  ],&lt;br&gt;
  "description": "awarding points",&lt;br&gt;
  "object_type": "event.processor"&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In this example, the processor receives an event from the accrual-send topic, invokes the prepare_json module, and sends the prepared result to the RestAPI topic.&lt;/p&gt;

&lt;h3&gt;
  
  
  PUSH
&lt;/h3&gt;

&lt;p&gt;The PUSH phase delivers the prepared package to an external destination.&lt;/p&gt;

&lt;p&gt;A transport action can use protocols such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SMTP&lt;/li&gt;
&lt;li&gt;REST&lt;/li&gt;
&lt;li&gt;Messenger&lt;/li&gt;
&lt;li&gt;Other transports added later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
  "topic": "accruals",&lt;br&gt;
  "actions": [&lt;br&gt;
    {&lt;br&gt;
      "module": "restapi_send",&lt;br&gt;
      "next_topic": "accrual-send",&lt;br&gt;
      "template_name": "accrual"&lt;br&gt;
    }&lt;br&gt;
  ],&lt;br&gt;
  "descriptions": "retrieving user points for payment processing",&lt;br&gt;
  "object_type": "event.transport"&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This handler consumes messages from the configured topic, invokes the restapi_send module, and can use the configured accrual template when preparing the destination request.&lt;/p&gt;

&lt;p&gt;The Event Processor keeps its own internal headers separate from headers required by an external destination. This matters because a third-party REST API may require its own authorization or protocol-specific headers that should not be confused with service-to-service context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configurable handlers and actions
&lt;/h2&gt;

&lt;p&gt;The Event Processor is configuration-driven.&lt;/p&gt;

&lt;p&gt;Its configuration API manages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event-handler configurations&lt;/li&gt;
&lt;li&gt;Email-sender configurations&lt;/li&gt;
&lt;li&gt;Message templates&lt;/li&gt;
&lt;li&gt;Access-token configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These records are stored in the configuration registry. They are loaded as active records and can be scoped by project.&lt;/p&gt;

&lt;p&gt;Configurations allow routes, actions, templates, delivery settings, and access details to be managed outside the core event-ingress endpoint.&lt;/p&gt;

&lt;p&gt;For example, templates can be loaded by project and template name. Email configuration can be retrieved separately from event-handler configuration. Access configuration can be stored per project rather than embedded in service code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handler lifecycle
&lt;/h2&gt;

&lt;p&gt;At startup, the service loads active configurations from the configuration registry.&lt;/p&gt;

&lt;p&gt;The current handler configuration types include:&lt;br&gt;
event.processor&lt;br&gt;
event.transport&lt;br&gt;
event.handler&lt;/p&gt;

&lt;p&gt;Each configuration creates an event handler with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Kafka topic to consume&lt;/li&gt;
&lt;li&gt;A handler type&lt;/li&gt;
&lt;li&gt;A Kafka consumer for that topic&lt;/li&gt;
&lt;li&gt;Access to the application-level producer&lt;/li&gt;
&lt;li&gt;A list of actions loaded from configuration&lt;/li&gt;
&lt;li&gt;A directory containing action modules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each action identifies a module that implements its logic and a next_topic for the action result.&lt;/p&gt;

&lt;p&gt;When a handler consumes an event, it runs the configured actions asynchronously. Action tasks are named with the event ID, handler topic, and action name, making concurrent work easier to trace during logging and debugging.&lt;/p&gt;

&lt;p&gt;Actions that return a result publish a new event to their configured downstream topic, where another handler can continue processing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Incoming event&lt;br&gt;
  → consume from topic&lt;br&gt;
    → run configured actions&lt;br&gt;
      → collect results&lt;br&gt;
        → publish results to next topics&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This allows processing to be expressed as a pipeline of independently configured stages rather than as one large synchronous workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration loading
&lt;/h2&gt;

&lt;p&gt;Configuration records are loaded from the configuration registry through asynchronous HTTP requests.&lt;/p&gt;

&lt;p&gt;The loaders select only active records. They filter by object_type and optionally by project_id. This allows multiple projects to use the same Event Processor while maintaining separate handler configurations, templates, email settings, and access configurations.&lt;/p&gt;

&lt;p&gt;The current API supports creating and updating event configurations. A planned improvement is to notify the event-handler container when a configuration changes so the relevant handler can start or restart without a full service restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure handling
&lt;/h2&gt;

&lt;p&gt;The intended model treats delivery outcomes as events.&lt;/p&gt;

&lt;p&gt;After a PUSH operation, the system can produce outcomes such as:&lt;br&gt;
_success&lt;br&gt;
_fail&lt;/p&gt;

&lt;p&gt;A more flexible configuration can specify which event should be emitted after successful or failed delivery.&lt;/p&gt;

&lt;p&gt;Failure behavior can also depend on an external response, such as an HTTP status code or an application-specific error code like USER_NOT_FOUND.&lt;/p&gt;

&lt;p&gt;Potential configured responses include:&lt;br&gt;
RETRY&lt;br&gt;
STOP&lt;/p&gt;

&lt;p&gt;A retry policy can specify a delay before another delivery attempt. A stop policy ends processing for that delivery.&lt;/p&gt;

&lt;p&gt;These capabilities describe the intended direction of the Event Processor. They should be understood as planned behavior until the associated retry, result-event, and failure-routing logic is implemented and verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signup email example
&lt;/h2&gt;

&lt;p&gt;The signup workflow ends by sending an event after user and profile creation succeeds.&lt;/p&gt;

&lt;p&gt;The Signup Service does not send the email directly. It publishes an event containing the information required by the configured event-processing pipeline.&lt;/p&gt;

&lt;p&gt;The Event Processor can then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive the event through the Event API&lt;/li&gt;
&lt;li&gt;Publish it to Kafka&lt;/li&gt;
&lt;li&gt;Consume it through a configured handler&lt;/li&gt;
&lt;li&gt;Prepare an email package using an action and template&lt;/li&gt;
&lt;li&gt;Send the email through a configured delivery mechanism&lt;/li&gt;
&lt;li&gt;Optionally publish a success or failure event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The registration workflow can complete independently of delivery, while notification behavior remains configurable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing perspective
&lt;/h2&gt;

&lt;p&gt;The Event Processor is built around a simple principle: applications publish facts about what happened, while configurable handlers decide what should happen next.&lt;/p&gt;

&lt;p&gt;The Event API controls event ingress. Kafka carries events between stages. The configuration registry stores handler rules, templates, delivery settings, and access details. Action modules prepare or deliver messages.&lt;/p&gt;

&lt;p&gt;The architecture is still evolving. The shared application-level Kafka producer is already in place, while dynamic handler reloads, configurable retries, and success or failure events are planned improvements.&lt;/p&gt;

&lt;p&gt;In the next article, I will discuss the service-to-service security model: tokens, authentication, authorization, and permissions.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-jwt-tokens-identity-context-and-permissions-3266"&gt;JWT Tokens&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Our System Series: Sensitive Data Encryption</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Tue, 22 Sep 2026 13:24:21 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-sensitive-data-encryption-36m5</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-sensitive-data-encryption-36m5</guid>
      <description>&lt;p&gt;Previous: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-registries-from-the-network-discovery-service-to-reusable-registries-2knl"&gt;Registries&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The registry layer gives us a consistent way to store and retrieve users, profiles, links, and other entities. The next question is how to protect sensitive values before those entities reach the database.&lt;/p&gt;

&lt;p&gt;The answer in this system is to encrypt selected fields in the User Profiles Service. As shown in the signup workflow, this service sits between the Signup Service and User Profiles Registries. It prepares user and profile data before it is persisted, including encryption of protected values.&lt;/p&gt;

&lt;p&gt;The registry receives encrypted values and stores them through its normal interface. It does not need to understand which fields are sensitive or implement encryption rules itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encrypting at the registry boundary
&lt;/h2&gt;

&lt;p&gt;The encryption module is implemented as decorators around the shared registry-interaction function:&lt;br&gt;
&lt;code&gt;@encrypt_payload&lt;br&gt;
@decrypt_payload&lt;br&gt;
async def interact_with_registry(...):&lt;br&gt;
    ...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Before a POST, PUT, or PATCH request is sent, the encryption decorator examines the target registry and encrypts the relevant fields. After a GET request completes, the decryption decorator reverses that transformation before the result is returned to the User Profiles Service.&lt;/p&gt;

&lt;p&gt;The registry name determines which rules apply. &lt;/p&gt;

&lt;p&gt;The same distinction is applied when decrypting individual user and profile records, as well as collection responses from users and profiles registries.&lt;/p&gt;

&lt;p&gt;This approach keeps encryption close to the service that understands the meaning of the data. It also prevents encryption and decryption calls from being copied into every method that interacts with user or profile records.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is encrypted
&lt;/h2&gt;

&lt;p&gt;The system uses field-level encryption rather than encrypting the complete record. Some profile data is nested. For nested profile data, the module encrypts the values inside media, email, and phone entries while preserving the surrounding JSON structure.&lt;/p&gt;

&lt;p&gt;The record’s overall structure remains available. A profile can still be identified, associated with a project, checked for status, and connected to other entities. What is protected are the values that reveal personal information.&lt;/p&gt;

&lt;h2&gt;
  
  
  The encryption format
&lt;/h2&gt;

&lt;p&gt;The module uses AESSIV from the Python cryptography package and creates a 512-bit encryption key.&lt;/p&gt;

&lt;p&gt;Before encryption, a value is serialized as JSON. This allows the same function to work with strings and structured values. The ciphertext is then encoded with URL-safe Base64 before it is stored in a registry record. That makes the encrypted result safe to place inside JSON payloads and send through the registry API.&lt;/p&gt;

&lt;p&gt;AES-SIV is an authenticated-encryption construction. In this implementation, no nonce is supplied, so the encryption is deterministic: encrypting the same normalized value with the same key produces the same ciphertext. The cryptography documentation describes this usage as deterministic authenticated encryption.&lt;/p&gt;

&lt;p&gt;That property is useful because it allows the system to search protected values by encrypting the search term first. The trade-off is that identical plaintext values produce identical ciphertext values, revealing equality between stored values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking key versions
&lt;/h2&gt;

&lt;p&gt;When the module encrypts at least one field, it records the active key version in the object metadata.&lt;/p&gt;

&lt;p&gt;When a record is returned from the registry, the module checks this value before attempting decryption.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A record with the current key version is decrypted.&lt;/li&gt;
&lt;li&gt;A record without a key_version is returned unchanged.&lt;/li&gt;
&lt;li&gt;A record marked with another key version is not decrypted with the current key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This supports a transition in which legacy plaintext records and newly encrypted records can coexist. It also gives every encrypted record the information needed for a future key-rotation process.&lt;/p&gt;

&lt;p&gt;At present, the module decrypts records only when their version matches the key configured in the service. A full rotation design would later need to support reading older versions, re-encrypting records in batches, and retiring older keys after the migration is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching protected profile data
&lt;/h2&gt;

&lt;p&gt;Encryption changes how the system searches user and profile records.&lt;/p&gt;

&lt;p&gt;A normal database query cannot compare a plaintext value such as Ivan with an encrypted value stored in the profile record. To handle this, profile search is performed twice.&lt;/p&gt;

&lt;p&gt;First, the service searches legacy records using plaintext filters. Then it normalizes and encrypts the same values and performs a second search for encrypted records.&lt;/p&gt;

&lt;p&gt;For example, a name is normalized before encryption and the resulting ciphertext is then used in an exact registry lookup. &lt;/p&gt;

&lt;p&gt;Phone numbers are normalized and validated before encryption. Email addresses are converted to lowercase first. This is necessary because deterministic encryption only produces matching ciphertext when the input is identical.&lt;/p&gt;

&lt;p&gt;The two result sets are merged by profile_id, so the same profile is not returned twice.&lt;/p&gt;

&lt;p&gt;This allows plaintext and encrypted records to coexist while encryption is introduced gradually rather than requiring every historical record to be migrated at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the design preserves
&lt;/h2&gt;

&lt;p&gt;The purpose of the module is not only to encrypt data. It must also preserve the behavior needed by the application.&lt;/p&gt;

&lt;p&gt;The design allows the system to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encrypt selected personal and authentication-related fields before storage&lt;/li&gt;
&lt;li&gt;Keep encryption rules in one shared module&lt;/li&gt;
&lt;li&gt;Continue using the same registry APIs&lt;/li&gt;
&lt;li&gt;Read legacy unencrypted records during the transition&lt;/li&gt;
&lt;li&gt;Find encrypted records through normalized exact-match searches&lt;/li&gt;
&lt;li&gt;Identify which key version was used for a record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is an important limitation: deterministic encryption allows equality matching, but not partial matching. Searches such as “starts with,” “contains,” or fuzzy text matching cannot operate directly on the encrypted values. The encrypted search path therefore uses exact comparisons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing perspective
&lt;/h2&gt;

&lt;p&gt;The User Profiles Service applies encryption at the point where sensitive user and profile data enters or leaves the registry layer. This keeps protected values out of plaintext database storage while allowing the registry architecture to remain standardized and reusable.&lt;/p&gt;

&lt;p&gt;The next step for this design is a formal key-rotation workflow: support multiple key versions for decryption, re-encrypt records in controlled batches, verify that the migration succeeded, and retire the old key only after the process is complete. These tasks are in the development pipeline.&lt;/p&gt;

&lt;p&gt;In the next article, I will describe the Event Processor. During user signup, the Signup Service sends an event after user and profile creation is complete. The Event Processor receives that event and handles follow-up work, including sending the signup email to the new user.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-event-processing-from-business-events-to-external-actions-4d9p"&gt;Event Processing&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>security</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Our System Series: Registries. From the Network Discovery Service to Reusable Registries</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Mon, 21 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-registries-from-the-network-discovery-service-to-reusable-registries-2knl</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-registries-from-the-network-discovery-service-to-reusable-registries-2knl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feueojydaav7el3t4grxh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feueojydaav7el3t4grxh.png" alt="Registries entities" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyff4p3o5k5z8l04jq1zr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyff4p3o5k5z8l04jq1zr.png" alt="Registries links" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Previous: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-user-signup-creating-users-profiles-and-relationships-108"&gt;User Signup Service&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Network Discovery Service
&lt;/h2&gt;

&lt;p&gt;The idea of registries originated in an application I will call the Network Discovery Service, internally named Kroozheva.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This requirement influenced the database design from the beginning.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Instead, we designed a generic model based on entities and links:&lt;br&gt;
Entity ─── Link ─── Entity&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This gave us a flexible data model for representing networks without creating a new relationship structure for every combination of entity types.&lt;/p&gt;

&lt;h2&gt;
  
  
  From the data model to registries
&lt;/h2&gt;

&lt;p&gt;Once the generic entity model had been defined, we needed a consistent way to access the stored objects.&lt;/p&gt;

&lt;p&gt;Every new entity type required the same basic operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create records&lt;/li&gt;
&lt;li&gt;Retrieve individual records&lt;/li&gt;
&lt;li&gt;Search and list records&lt;/li&gt;
&lt;li&gt;Update records&lt;/li&gt;
&lt;li&gt;Delete or deactivate records&lt;/li&gt;
&lt;li&gt;Perform batch operations where necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This led to the idea of registries: database-access services with a standard CRUD interface for a particular registry or table.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registry API
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying registry collections
&lt;/h2&gt;

&lt;p&gt;A registry is more useful than a basic CRUD interface because its collection endpoints support structured queries.&lt;/p&gt;

&lt;p&gt;A request can limit the number of returned records and specify the starting position:&lt;br&gt;
&lt;code&gt;GET /api/objects/?limit=10&lt;br&gt;
GET /api/objects/?limit=10&amp;amp;offset=5&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The first request returns ten records. The second starts at offset five and returns the next page.&lt;/p&gt;

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

&lt;p&gt;Several filters can be combined:&lt;br&gt;
'GET /api/objects/?limit=10&amp;amp;offset=5&amp;amp;object_type=person&amp;amp;name=Ivan'&lt;/p&gt;

&lt;p&gt;This returns one page of objects matching both the object type and name.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Querying flexible data
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;br&gt;
&lt;code&gt;GET /api/objects/?data=username__exact=Ivan&lt;br&gt;
GET /api/objects/?data=number__gte=18::int&lt;br&gt;
GET /api/objects/?data=number__lt=12.5::float&lt;/code&gt;&lt;br&gt;
The query identifies the JSON key, the lookup operation, and, when necessary, the expected value type.&lt;/p&gt;

&lt;p&gt;Supported lookups include:&lt;br&gt;
icontains&lt;br&gt;
exact&lt;br&gt;
iexact&lt;br&gt;
gt&lt;br&gt;
gte&lt;br&gt;
lt&lt;br&gt;
lte&lt;br&gt;
endswith&lt;br&gt;
iendswith&lt;br&gt;
startswith&lt;br&gt;
istartswith&lt;/p&gt;

&lt;p&gt;Several JSON-field filters can be combined:&lt;br&gt;
&lt;code&gt;GET /api/objects/?data=username__icontains=a&amp;amp;data=some_id__exact=23&lt;/code&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Searching and ordering
&lt;/h2&gt;

&lt;p&gt;The API also supports general search.&lt;/p&gt;

&lt;p&gt;Without a field restriction, the search value is applied across all searchable fields:&lt;br&gt;
&lt;code&gt;GET /api/objects/?search=2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A search can be limited to one field:&lt;br&gt;
&lt;code&gt;GET /api/objects/?search=2&amp;amp;field=object_code&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It can also target several fields:&lt;br&gt;
&lt;code&gt;GET /api/objects/?search=350&amp;amp;field=object_code&amp;amp;field=project_id&amp;amp;field=data&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nested JSON fields can be addressed with double-underscore notation:&lt;br&gt;
&lt;code&gt;GET /api/objects/?search=+78564523636&amp;amp;field=data__telephone&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Results can be ordered explicitly:&lt;br&gt;
&lt;code&gt;GET /api/objects/?ordering=object_code&lt;br&gt;
GET /api/objects/?ordering=-object_code&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Multiple ordering fields are supported:&lt;br&gt;
&lt;code&gt;GET /api/objects/?ordering=object_code,created_date&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ordering can also use values inside the JSON data field:&lt;br&gt;
&lt;code&gt;GET /api/objects/?ordering=data__number,-data__date&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The default ordering is meta__internal_id in descending order, so newer objects appear first unless another ordering is requested.&lt;/p&gt;

&lt;p&gt;Pagination, filtering, searching, and ordering make the registry API useful for both ordinary application requests and larger data-processing operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The registry interaction module
&lt;/h2&gt;

&lt;p&gt;Application services do not construct registry requests independently. We use a shared module that centralizes communication with registry services.&lt;/p&gt;

&lt;p&gt;Its main entry point is:&lt;br&gt;
&lt;code&gt;async def interact_with_registry(...)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The function receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The incoming FastAPI request&lt;/li&gt;
&lt;li&gt;The HTTP method&lt;/li&gt;
&lt;li&gt;The registry base URL&lt;/li&gt;
&lt;li&gt;The registry name&lt;/li&gt;
&lt;li&gt;An optional object ID&lt;/li&gt;
&lt;li&gt;Query parameters&lt;/li&gt;
&lt;li&gt;Request data&lt;/li&gt;
&lt;li&gt;Filtering and error-handling options&lt;/li&gt;
&lt;li&gt;A request timeout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This keeps registry interaction consistent across application services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request construction
&lt;/h2&gt;

&lt;p&gt;The module supports the standard HTTP methods:&lt;br&gt;
GET&lt;br&gt;
POST&lt;br&gt;
PUT&lt;br&gt;
PATCH&lt;br&gt;
DELETE&lt;/p&gt;

&lt;p&gt;When an object ID is not supplied, the module builds a collection URL:&lt;br&gt;
&lt;code&gt;{registry_url}/{registry_name}/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When an ID is supplied, it builds an individual-object URL:&lt;br&gt;
'{registry_url}/{registry_name}/{id}/'&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeouts and failures
&lt;/h2&gt;

&lt;p&gt;The module obtains the HTTP client from the FastAPI application and applies a configurable timeout to every request.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The response body is parsed as JSON. If parsing fails, the module raises IncorrectFormatInRegistryException&lt;/p&gt;

&lt;p&gt;The module also handles several registry-level conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 404 response or empty response can become NotFoundException&lt;/li&gt;
&lt;li&gt;A duplicate-object response containing exists becomes EntityExistsException&lt;/li&gt;
&lt;li&gt;A hidden or deactivated object can become RegistryObjectDeactivatedException&lt;/li&gt;
&lt;li&gt;Other unsuccessful responses become RegistryInteractionException&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The active_records and raise_not_found parameters allow a caller to adjust this behavior for a particular operation.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a registry
&lt;/h2&gt;

&lt;p&gt;The Registry Factory was created to reduce the repetitive work involved in introducing new registries.&lt;/p&gt;

&lt;p&gt;A developer creates a registry by running a Django management command:&lt;br&gt;
&lt;code&gt;python manage.py createregistry --template=registry_template.zip people --model_name=Person&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The command receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The template to use&lt;/li&gt;
&lt;li&gt;The registry name, such as people&lt;/li&gt;
&lt;li&gt;The model name, such as Person&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The factory generates the application structure for the new registry.&lt;/p&gt;

&lt;p&gt;The generated registry is then added to INSTALLED_APPS:&lt;br&gt;
&lt;code&gt;INSTALLED_APPS = [&lt;br&gt;
    "registry_config",&lt;br&gt;
    "links",&lt;br&gt;
    "rest_framework",&lt;br&gt;
    "django_filters",&lt;br&gt;
    "people",&lt;br&gt;
]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The developer creates and applies database migrations:&lt;br&gt;
&lt;code&gt;python manage.py makemigrations&lt;br&gt;
python manage.py migrate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The generated viewsets are then registered with the project router:&lt;br&gt;
&lt;code&gt;router.register(r"people", PeopleViewset)&lt;br&gt;
router.register(r"person", PersonViewset)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This exposes collection and individual-object endpoints using the same convention as existing registries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and documentation
&lt;/h2&gt;

&lt;p&gt;The registry can be tested independently:&lt;br&gt;
&lt;code&gt;python manage.py test people&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A coverage report can be generated with:&lt;br&gt;
&lt;code&gt;coverage run manage.py test people&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The service can then be started locally:&lt;br&gt;
&lt;code&gt;python manage.py runserver&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Its API can be inspected through Swagger UI.&lt;/p&gt;

&lt;p&gt;The project wiki documents this workflow, including registry creation, project integration, migrations, testing, and API access.&lt;/p&gt;

&lt;p&gt;The documentation serves two audiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers creating a new registry&lt;/li&gt;
&lt;li&gt;Developers integrating with an existing registry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  From command to working API
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The factory provides the common structure. Developers can then add entity-specific fields, validation, permissions, and business rules where necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generic persistence and business logic
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This standardization allows application services to work with different registries through the same interaction patterns.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-sensitive-data-encryption-36m5"&gt;Sensitive Data Encryption&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Our System Series: User Signup. Creating Users, Profiles, and Relationships</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Fri, 18 Sep 2026 12:09:31 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-user-signup-creating-users-profiles-and-relationships-108</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-user-signup-creating-users-profiles-and-relationships-108</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8zdaoew95jn6d8mskpu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8zdaoew95jn6d8mskpu.png" alt="User signup part 1" width="800" height="646"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9yh98rckm87j3w62ar6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9yh98rckm87j3w62ar6.png" alt="User signup part 2" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Previous: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-architecture-overview-5d6b"&gt;Architecture Overview&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the previous article, I introduced the four-tier architecture used by our system and described its graph-like data model. This article shows how those ideas come together in a practical business workflow: user registration.&lt;/p&gt;

&lt;p&gt;User registration is not limited to creating a single database record. The workflow creates a user, associates that user with a project and an account, creates a profile, associates the profile with the project, and triggers a signup email.&lt;/p&gt;

&lt;p&gt;The Signup Service coordinates the workflow, while the User Profiles Service and User Profiles Registries handle user-related data operations.&lt;/p&gt;

&lt;p&gt;The complete interaction is shown in the accompanying diagrams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User signup, part 1.&lt;/li&gt;
&lt;li&gt;User signup, part 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Separating users and profiles
&lt;/h2&gt;

&lt;p&gt;The system separates user and profile entries because they contain different categories of information.&lt;/p&gt;

&lt;p&gt;A user entry contains system identity and authentication-related information. It includes the user ID, user type, registration date, login, last-login timestamp, account-status flags, and the profile_id of the associated profile.&lt;/p&gt;

&lt;p&gt;A profile entry contains personal and project-related information. It includes the profile ID, profile type, first and last names, birth date, sex, contact phone numbers, contact email addresses, main photo, project ID, and additional information such as education, specialty, registration address, and personal-data consent.&lt;/p&gt;

&lt;p&gt;The user entry references the profile through profile_id. In the usual case, user_id and profile_id have the same value. This makes it easy to correlate the two records while keeping their responsibilities separate.&lt;/p&gt;

&lt;p&gt;This separation establishes a clear boundary between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Authentication and system identity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Personal and user-facing information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Account and project relationships.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access control and profile management.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Authentication-related operations can work with the user entry without loading the full profile. Profile-related operations can work with personal data without exposing authentication-specific fields such as login status or password-management flags.&lt;/p&gt;

&lt;p&gt;The user and profile are separate registry entries, but the profile_id field provides their direct association. Other relationships, such as the user’s association with an account or project, are represented through link records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration flow
&lt;/h2&gt;

&lt;p&gt;The registration process is divided into two stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creating the user and establishing the user’s relationships.&lt;/li&gt;
&lt;li&gt;Creating the profile and triggering the signup email.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client sends the registration request to the Signup Service. The request contains user and profile data, together with Project-ID and Account-ID headers that provide the context for the new registration.&lt;/p&gt;

&lt;p&gt;The Signup Service coordinates the process but does not directly access the database. It delegates user and profile operations to the User Profiles Service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the user
&lt;/h2&gt;

&lt;p&gt;The User Profiles Service receives the request to create a user and encrypts the relevant data before persistence.&lt;/p&gt;

&lt;p&gt;It then asks User Profiles Registries to create a user entry. The registry service inserts the record into the users table and returns the result.&lt;/p&gt;

&lt;p&gt;The user entry contains system and authentication-related data, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User ID&lt;/li&gt;
&lt;li&gt;User type&lt;/li&gt;
&lt;li&gt;Registration date&lt;/li&gt;
&lt;li&gt;Login&lt;/li&gt;
&lt;li&gt;Last-login timestamp&lt;/li&gt;
&lt;li&gt;Account-status flags&lt;/li&gt;
&lt;li&gt;Associated profile ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the user entry has been created, the User Profiles Service creates two relationships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A project-user link&lt;/li&gt;
&lt;li&gt;An account-user link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These links associate the new user with the project and account identified by the request headers.&lt;/p&gt;

&lt;p&gt;The relationships are stored as link records rather than as separate relationship tables. This is an example of the graph-like data model introduced in the architecture overview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the profile
&lt;/h2&gt;

&lt;p&gt;After the user has been created successfully, the Signup Service asks the User Profiles Service to create the profile.&lt;/p&gt;

&lt;p&gt;The User Profiles Service encrypts the relevant profile data and requests a profile entry from User Profiles Registries. The registry service stores the entry in the profiles table.&lt;/p&gt;

&lt;p&gt;The profile entry contains personal and project-related data, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Profile ID and profile type&lt;/li&gt;
&lt;li&gt;First name and last name&lt;/li&gt;
&lt;li&gt;Birth date and sex&lt;/li&gt;
&lt;li&gt;Contact phone numbers&lt;/li&gt;
&lt;li&gt;Contact email addresses&lt;/li&gt;
&lt;li&gt;Main profile photo&lt;/li&gt;
&lt;li&gt;Additional information such as education, specialty, registration address, and personal-data consent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The profile receives the identifier referenced by the user’s profile_id. In the usual case, the user ID and profile ID are the same UUID, which provides a simple one-to-one association between the two records.&lt;/p&gt;

&lt;p&gt;The User Profiles Service then creates a project-profile link, associating the profile with the project supplied during registration.&lt;/p&gt;

&lt;p&gt;At this point, the database contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user entry containing system and authentication information&lt;/li&gt;
&lt;li&gt;A profile entry containing personal information&lt;/li&gt;
&lt;li&gt;An account-user link&lt;/li&gt;
&lt;li&gt;A project-user link&lt;/li&gt;
&lt;li&gt;A project-profile link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After these operations succeed, the Signup Service returns the user ID and profile ID to the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signup email event
&lt;/h2&gt;

&lt;p&gt;Once the user and profile records have been created, the Signup Service sends a signup email event to the Event Processor.&lt;/p&gt;

&lt;p&gt;The Event Processor processes the event and sends the signup email to the user.&lt;/p&gt;

&lt;p&gt;Separating email delivery from the registration workflow keeps the Signup Service focused on creating the required entities and relationships. Email processing can be handled independently by the Event Processor rather than being implemented as part of the main registration logic.&lt;/p&gt;

&lt;p&gt;This separation also creates the possibility of handling email-specific concerns independently, such as retries, provider integration, and delivery monitoring. The exact behavior depends on the event-processing implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design considerations
&lt;/h2&gt;

&lt;p&gt;This workflow illustrates several architectural decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User identity and personal profile data are stored separately&lt;/li&gt;
&lt;li&gt;The user entry references its profile through profile_id&lt;/li&gt;
&lt;li&gt;The same UUID is usually used for both the user and profile identifiers&lt;/li&gt;
&lt;li&gt;Account and project relationships are represented as reusable link records&lt;/li&gt;
&lt;li&gt;Application services coordinate business workflows&lt;/li&gt;
&lt;li&gt;Registry services provide generic database operations&lt;/li&gt;
&lt;li&gt;Protected data is encrypted before persistence&lt;/li&gt;
&lt;li&gt;Email delivery is delegated to a separate event-processing component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to keep responsibilities explicit. The Signup Service does not need to know how every table is implemented, and the registry service does not need to understand the complete business meaning of registration. Each component performs a focused part of the overall workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing perspective
&lt;/h2&gt;

&lt;p&gt;User signup demonstrates how the system’s architecture and data model work together in a real business scenario.&lt;/p&gt;

&lt;p&gt;A single registration creates several related records, but the process remains organized because each service has a defined responsibility. The Signup Service coordinates the workflow, the User Profiles Service manages user-related operations, the registry layer handles reusable persistence, and the Event Processor handles post-registration communication.&lt;/p&gt;

&lt;p&gt;The separation between users and profiles is particularly important. Authentication and system identity are kept in the user registry, while personal and project-related information is kept in the profile registry. The profile_id field connects the two records, and the shared identifier convention makes the relationship straightforward to resolve.&lt;/p&gt;

&lt;p&gt;The next article will focus on registries: what they are, why they were introduced, and how the Registry Factory reduces the amount of repetitive CRUD code required when adding new entity types.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-registries-from-the-network-discovery-service-to-reusable-registries-2knl"&gt;Registries&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Our System Series: Architecture Overview</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Fri, 18 Sep 2026 11:53:44 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-architecture-overview-5d6b</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-architecture-overview-5d6b</guid>
      <description>&lt;p&gt;Previous: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/introduction-to-our-system-series-56p1"&gt;Introduction to Our System Series&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our system uses a four-tier microservices architecture:&lt;br&gt;
Frontend → Application Services → Registry Services → Database&lt;/p&gt;

&lt;p&gt;Each tier has a specific responsibility. The frontend handles user interaction, application services implement business and supporting workflows, registry services provide reusable database operations, and the database stores entities and their relationships.&lt;/p&gt;

&lt;p&gt;The system uses a graph-like data model consisting of entities and links. Entities can represent users, projects, accounts, or virtually any other type of object required by the application. Links represent relationships between entities.&lt;/p&gt;

&lt;p&gt;Instead of creating a separate relationship table for every possible pair of entities, the system uses a general links structure. A link contains the identifiers of the connected entities, its direction, type, weight, and any additional information stored as JSON.&lt;/p&gt;

&lt;p&gt;This approach allows us to introduce new entity types and relationships without redesigning the entire database schema. It also provides a flexible foundation for representing complex networks of connected objects.&lt;/p&gt;

&lt;p&gt;In the following articles, I will explain the responsibilities of the application and registry services in more detail. I will use the Signup Service as an example to show how the different tiers interact during a typical business workflow.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-user-signup-creating-users-profiles-and-relationships-108"&gt;User Signup&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Introduction to Our System Series</title>
      <dc:creator>Ilya Mikhasik</dc:creator>
      <pubDate>Fri, 18 Sep 2026 11:50:40 +0000</pubDate>
      <link>https://dev.to/ilya_mikhasik_f8c2953d624/introduction-to-our-system-series-56p1</link>
      <guid>https://dev.to/ilya_mikhasik_f8c2953d624/introduction-to-our-system-series-56p1</guid>
      <description>&lt;p&gt;Over the past few years, I have worked on several backend systems involving microservices, databases, APIs, data protection, service integration. I have often used these technologies as part of my daily work, but using a technology and being able to explain its role clearly are not the same thing.&lt;/p&gt;

&lt;p&gt;I am writing this series to take a more structured look at the systems I have been developing and the technologies behind them. Rather than creating a simple list of frameworks and tools, I want to describe how they work together in a real application and why particular design decisions were made.&lt;/p&gt;

&lt;p&gt;The series will begin with the architecture itself. I will explain how the system is divided into frontend, application, registry, and database layers, and how responsibilities are distributed between them. I will also discuss the graph-like data model used to represent entities and their relationships, as well as the Registry Factory I developed to automate the creation of CRUD services.&lt;/p&gt;

&lt;p&gt;After that, I will describe several typical business scenarios. Each scenario will be supported by service diagrams showing how requests move through the system. I will explain what each service does, how the services communicate, how data is stored, and what happens when a workflow succeeds or fails.&lt;/p&gt;

&lt;p&gt;This is also a way for me to take stock of my own experience. Writing about a system forces me to move beyond naming technologies and explain how I used them to solve concrete engineering problems. It helps identify what I understand well, what I need to investigate further, and which parts of my experience are most relevant to future projects.&lt;/p&gt;

&lt;p&gt;The articles are not intended to present the architecture as universally correct. Every system reflects its requirements, constraints, and history. My goal is instead to describe the design honestly, explain the decisions in their original context, and reflect on what worked, what was difficult, and what could be improved.&lt;/p&gt;

&lt;p&gt;I will begin with an overview of the architecture and then use user registration as the first example of how the different parts of the system work together.&lt;/p&gt;

&lt;p&gt;Next: &lt;a href="https://dev.to/ilya_mikhasik_f8c2953d624/our-system-series-architecture-overview-5d6b"&gt;Architecture Overview&lt;/a&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>api</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
