How Macro's Contact Service Manages Relationships Between Users
Macro's contact service manages relationships between users through an event-driven architecture that processes two distinct message types—ContactsNodes for complete graph updates and ContactConnections for explicit pairwise relationships—via an async SQS pipeline to ensure scalable, eventually consistent relationship storage.
Macro's contact service, implemented in the macro-inc/macro repository, provides a robust mechanism for establishing and querying social connections between users. The system utilizes specific message structures defined in the domain layer to represent relationship intent, which are then processed asynchronously to maintain data integrity and cache coherence. This architecture separates the ingestion of relationship requests from their persistence, enabling reliable handling of both bulk graph operations and individual connection updates.
Core Data Models for Relationship Management
The service defines two primary message structures in [crates/contacts/src/domain/models/messages.rs](https://github.com/macro-inc/macro/blob/main/crates/contacts/src/domain/models/messages.rs) to represent relationship semantics.
Complete Graph Updates with ContactsNodes
The ContactsNodes struct represents a complete graph operation where every user in a set becomes contacts with every other user in that set. This model uses a HashSet to store unique user identifiers, ensuring idempotent processing of bulk relationship creation.
pub struct ContactsNodes {
pub users: HashSet<MacroUserIdStr<'static>>,
}
This structure is ideal for scenarios such as adding a new member to a workspace or group where mutual contact relationships must be established immediately between all participants.
Explicit Pairwise Connections with ContactConnections
For undirected edges between two specific users, the service uses ContactConnection and its wrapper ContactConnections. The ContactConnection struct explicitly defines the two endpoints of a relationship, while ContactConnections batches multiple explicit relationships into a single message for efficient processing.
pub struct ContactConnection {
pub first: MacroUserIdStr<'static>,
pub second: MacroUserIdStr<'static>,
}
pub struct ContactConnections {
pub connections: Vec<ContactConnection>,
}
These structures support fine-grained relationship management where individual connections are created or removed independently of any broader group context.
Service Ports and Abstraction Layer
The domain logic is abstracted through a set of port traits defined in [crates/contacts/src/domain/ports.rs](https://github.com/macro-inc/macro/blob/main/crates/contacts/src/domain/ports.rs). This hexagonal architecture decouples the business logic from infrastructure concerns.
- ContactsRepository – Handles database persistence for contact queries and mutations.
- ContactsNotifier – Invalidates cached contact data after relationship mutations to ensure read consistency.
- ContactsIngressQueue – Publishes
ContactsNodesorContactConnectionsto the inbound SQS queue viapublish_nodesandpublish_connectionsmethods. - ContactsIngress – Provides the high-level façade
enqueue_contactsandenqueue_contact_connectionsfor application handlers to submit relationship requests. - ContactsService – Exposes query capabilities such as
query_contactsandadd_contact_nodesfor API consumers.
Async Processing Pipeline
The contact service implements an event-driven pipeline that ensures reliable processing of relationship updates even under high load.
- API handlers receive HTTP requests and construct either
ContactsNodesorContactConnectionsmessages based on the operation type. - The handler invokes ContactsIngress::enqueue_contacts or enqueue_contact_connections, which serializes the message and delegates to the ingress queue implementation.
- ContactsIngressQueue publishes the message to the dedicated contacts SQS queue, providing durable storage until processing completes.
- An inbound worker consumes messages from the queue and deserializes them into the
ContactsMessageenum. - The worker invokes ContactsRepository to persist the relationships:
- For
Nodesmessages, the repository creates pairwise connections for all combinations in the set. - For
Connectionsmessages, the repository inserts explicit undirected edges.
- For
- After successful persistence, ContactsNotifier invalidates cached contact lists for affected users, ensuring subsequent queries return fresh data.
This separation of concerns allows the API to remain responsive while complex graph operations are processed asynchronously.
Practical Implementation Examples
Creating a Complete Graph of Contacts
Use ContactsNodes when all users in a set should become mutual contacts, such as onboarding a new team member to an existing group:
use macro_user_id::user_id::MacroUserIdStr;
use std::collections::HashSet;
use contacts::domain::models::messages::ContactsNodes;
use contacts::domain::ports::ContactsIngress;
// Build the set of user IDs that should all be mutually connected.
let mut users = HashSet::new();
users.insert(MacroUserIdStr::new("user-a"));
users.insert(MacroUserIdStr::new("user-b"));
users.insert(MacroUserIdStr::new("user-c"));
let nodes_msg = ContactsNodes { users };
// Enqueue the complete-graph message for async processing.
contacts_ingress.enqueue_contacts(nodes_msg).await?;
Establishing an Explicit Pairwise Relationship
Use ContactConnection to create a single undirected relationship between two specific users:
use contacts::domain::models::messages::{ContactConnection, ContactConnections};
use contacts::domain::ports::ContactsIngress;
// Build a single relationship.
let conn = ContactConnection {
first: MacroUserIdStr::new("user-x"),
second: MacroUserIdStr::new("user-y"),
};
let connections_msg = ContactConnections { connections: vec![conn] };
// Enqueue only this explicit relationship.
contacts_ingress.enqueue_contact_connections(connections_msg.connections).await?;
Querying a User’s Contact List
Retrieve the current contacts for a user through the service façade:
use contacts::domain::ports::ContactsService;
use macro_user_id::user_id::MacroUserIdStr;
// `contacts_service` is an implementation of the `ContactsService` trait.
let user_id = MacroUserIdStr::new("alice");
let contacts = contacts_service.query_contacts(user_id).await?;
println!("Alice’s contacts: {:?}", contacts);
Summary
- Macro's contact service uses two message types—
ContactsNodesfor complete graphs andContactConnectionsfor explicit edges—to represent relationship intent. - The ports and adapters architecture defined in
domain/ports.rsdecouples business logic from SQS, database, and cache implementations. - Async processing via SQS queues ensures reliable handling of relationship updates without blocking API responses.
- Cache invalidation through
ContactsNotifiermaintains data consistency after mutations. - All domain models and traits are centralized in the
crates/contacts/src/domain/directory for clear dependency management.
Frequently Asked Questions
What is the difference between ContactsNodes and ContactConnections?
ContactsNodes creates a complete graph where every user in the provided set becomes contacts with every other user in that set, useful for bulk operations like team onboarding. ContactConnections creates explicit undirected edges between specific pairs of users, providing fine-grained control over individual relationships. Both are processed asynchronously through the same pipeline but result in different database insertion patterns.
How does the contact service ensure cache consistency?
After the ContactsRepository successfully persists relationship changes, the ContactsNotifier trait triggers invalidation of cached contact lists for all affected user IDs. This ensures that subsequent calls to query_contacts retrieve fresh data from the database rather than stale cached values, maintaining eventual consistency across distributed components.
Where are the service interfaces and message types defined?
The core domain definitions reside in two primary files within the macro-inc/macro repository: [crates/contacts/src/domain/models/messages.rs](https://github.com/macro-inc/macro/blob/main/crates/contacts/src/domain/models/messages.rs) contains the ContactsNodes and ContactConnection structs, while [crates/contacts/src/domain/ports.rs](https://github.com/macro-inc/macro/blob/main/crates/contacts/src/domain/ports.rs) defines the ContactsService, ContactsIngress, and repository traits that structure the application's hexagonal architecture.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →