# How Macro's Contact Service Manages Relationships Between Users

> Discover how Macro's contact service manages user relationships using an event-driven architecture and an async SQS pipeline for scalable, consistent data.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: internals
- Published: 2026-08-18

---

**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)](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.

```rust
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.

```rust
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)](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 `ContactsNodes` or `ContactConnections` to the inbound SQS queue via `publish_nodes` and `publish_connections` methods.
*   **ContactsIngress** – Provides the high-level façade `enqueue_contacts` and `enqueue_contact_connections` for application handlers to submit relationship requests.
*   **ContactsService** – Exposes query capabilities such as `query_contacts` and `add_contact_nodes` for API consumers.

## Async Processing Pipeline

The contact service implements an event-driven pipeline that ensures reliable processing of relationship updates even under high load.

1.  **API handlers** receive HTTP requests and construct either `ContactsNodes` or `ContactConnections` messages based on the operation type.
2.  The handler invokes **ContactsIngress::enqueue_contacts** or **enqueue_contact_connections**, which serializes the message and delegates to the ingress queue implementation.
3.  **ContactsIngressQueue** publishes the message to the dedicated contacts SQS queue, providing durable storage until processing completes.
4.  An **inbound worker** consumes messages from the queue and deserializes them into the `ContactsMessage` enum.
5.  The worker invokes **ContactsRepository** to persist the relationships:
    *   For `Nodes` messages, the repository creates pairwise connections for all combinations in the set.
    *   For `Connections` messages, the repository inserts explicit undirected edges.
6.  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:

```rust
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:

```rust
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:

```rust
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**—`ContactsNodes` for complete graphs and `ContactConnections` for explicit edges—to represent relationship intent.
*   The **ports and adapters** architecture defined in [`domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/domain/ports.rs) decouples 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 `ContactsNotifier` maintains 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)](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)](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.