When Third-Party Integration Meets Domain Boundaries
Embedding Discord concerns within domains instead of extracting them to a dedicated service
Executive Summary
The company sells access to content published on a Discord server across multiple channels. The initial implementation relied on manual verification: employees confirm purchases and assign Discord roles by hand. As the product matures, this workflow needs automation.
Automating access requires integrating Discord into multiple existing domains: authorization needs role mappings, subscription management needs plan-to-role assignments, user management needs Discord identity storage, and registration needs OAuth orchestration. This raises an architectural question: should Discord become its own API, or should Discord concerns be embedded within these existing domains?
I chose embedding. Each domain owns the Discord concepts relevant to its responsibilities. This is intentional short-term debt driven by two architectural characteristics I prioritize: cost and agility. With a single small team and limited time, extracting a dedicated Discord API would introduce coordination overhead and slow discovery. The domain-embedded approach allows each domain to evolve its Discord integration independently.
Timeline: The decision and initial implementation took approximately one week. The architecture will evolve to a thin provider when specific triggers occur: multiple similar third-party integrations, team growth beyond a single team, or significantly increased API call volume.
This case study examines that decision, the alternatives considered, what would trigger evolution toward centralization, and why I consider this a rare example of true technical debt: intentional, with understood interest, and a clear payback plan.
The Business Context
The company operates as a content platform where customers purchase access to various products and services. One product grants access to a Discord server where the company publishes content and customers engage in community discussions.
This is the first time the company sells access to content published on a platform it doesnβt directly own. Previous products were delivered through infrastructure the company controlled. Discord introduces new variables: a third-party API, external identity systems, platform-specific access controls. This novelty is part of why discovery and agility were prioritized so heavily in the architectural decision.
The current workflow is entirely manual:
- Customer purchases a Discord access plan through the normal purchase flow
- Customer receives an email directing them to create a Discord account and join the company server
- Customer provides their registered email through a Discord workflow
- An employee manually verifies the customerβs plan in a spreadsheet
- The employee assigns the appropriate Discord role, granting channel access
- When a customerβs plan changes (upgrade, downgrade, cancellation), an employee must notice the change and manually update their Discord role
Discord roles control channel visibility. A βPremiumβ role might grant access to exclusive channels, while a βFreeβ role provides limited access. The mapping between subscription plans and Discord roles is the core business logic that needs automation.
Why This Matters
The manual process creates several problems:
- Delayed access: Customers wait for human verification before accessing content theyβve paid for
- Inconsistent state: Plan changes arenβt immediately reflected in Discord access
- Operational burden: Employee time spent on manual verification scales linearly with customer growth
- Error-prone: Spreadsheet-based tracking invites mistakes
The solution needs to automate the entire flow, including OAuth-based Discord linking, automatic role assignment based on plans, and real-time role updates when plans change.
The Existing Domain Architecture
Before discussing where Discord fits, hereβs the relevant portion of the existing architecture:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Prime (Frontend) β
β User-facing web application β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββββββββ βββββββββββββββββββββββββ βββββββββββββββββββββββββ
β Registrar API β β Subscription Mgmt β β Authorization API β
β β β API β β β
β β’ User registration β β β’ Plan management β β β’ Access rules β
β β’ Email verification β β β’ Billing integration β β β’ Product permissions β
β β’ Onboarding flows β β β’ Billing events β β β’ Feature flags β
βββββββββββββββββββββββββ βββββββββββββββββββββββββ βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββ
β Users API β
β β
β β’ User profiles β
β β’ Identity data β
β β’ GraphQL interface β
βββββββββββββββββββββββββ
Each API owns its domain completely. The Authorization API determines what users can access. The Subscription Management API handles plans and billing events from the payment provider. The Registrar API orchestrates user onboarding. The Users API stores identity and profile data.
The Options Considered
Option 1: Domain-Embedded Integration
Each domain owns the Discord concepts relevant to its responsibilities:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Prime β
β Discord OAuth UI components β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββββββββββββββββββββββ
β Registrar API β β Subscription Mgmt β β Authorization API β
β β β β β β
β Discord OAuth flow β β plan_discord_roles β β discord_roles table β
β Discord registration β β (which plans grant β β (role metadata, β
β β β which roles) β β assignability rules)β
β ββββββββββββββββ β β ββββββββββββββββ β β ββββββββββββββββ β
β βDiscord Clientβ β β βDiscord Clientβ β β βDiscord Clientβ β
β ββββββββββββββββ β β ββββββββββββββββ β β ββββββββββββββββ β
ββββββββββββββββββββββββ ββββββββββββββββββββββββ ββββββββββββββββββββββββ
β
User authorization
sync
β
βββββββββββββββββββββββββ
βΌ
ββββββββββββββββββββββββ
β Users API β
β β
β user_discord_accountsβ
β (Discord β user β
β identity mapping) β
ββββββββββββββββββββββββ
Data ownership:
Authorization API owns role metadata. This table defines which Discord roles exist, which can be assigned programmatically, and which serve as defaults for free or paid users:
CREATE TABLE discord_roles (
id INT NOT NULL AUTO_INCREMENT,
discord_id VARCHAR(30) NOT NULL,
name VARCHAR(100) NOT NULL,
is_assignable BOOLEAN NOT NULL DEFAULT 0,
is_plan_assignable BOOLEAN NOT NULL DEFAULT 0,
is_default_free BOOLEAN NULL,
is_default_paid BOOLEAN NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY UK_discord_roles_discord_id (discord_id)
);
Subscription Management API owns plan-to-role mappings. This determines which Discord roles a given subscription plan grants:
CREATE TABLE plan_discord_roles (
id INT NOT NULL AUTO_INCREMENT,
plan_id INT UNSIGNED NOT NULL,
role_id INT NOT NULL,
CONSTRAINT UC_RoleId_PlanId UNIQUE (plan_id, role_id),
CONSTRAINT FK_PlanDiscordRoles_Plans FOREIGN KEY (plan_id) REFERENCES plans(id),
PRIMARY KEY (id)
);
Users API owns the identity mapping between platform users and Discord accounts:
CREATE TABLE user_discord_accounts (
user_id INT NOT NULL,
discord_id VARCHAR(20) NOT NULL,
discord_username VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_discord_id (discord_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY (user_id)
);
Registrar API has no persistent Discord data. It orchestrates the OAuth flow and registration, then delegates storage to the Users API and triggers authorization sync.
Workflow:
- User initiates Discord linking through Prime
- Registrar API handles OAuth, obtains Discord identity
- Registrar API stores identity mapping via Users API
- Registrar API triggers authorization sync
- Authorization API reads userβs plans (from Subscription Management), determines correct roles, updates Discord
When a plan changes (billing webhook), the Subscription Management API triggers the same authorization sync, and roles update automatically.
What remains centralized:
Embedding client code in each domain doesnβt mean abandoning governance. Configuration and credentials are managed through AWS Parameter Store, giving us:
- Single source of truth for Discord API credentials
- Consistent client configuration (timeouts, retry policies) across all domains
- Centralized audit trail of configuration changes
- Environment-specific settings without code changes
The domain-embedded approach applies to code, not to operational governance. Security and configuration management arenβt βPhase 2β concerns.
Option 2: Thin Provider API
Centralize Discord API access while keeping business logic in domains:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Discord Provider API β
β β
β β’ Discord API client (credentials, rate limiting, retries) β
β β’ OAuth token management β
β β’ Role assignment operations β
β β’ Server membership operations β
β β’ No business logic about WHEN to assign roles β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β² β² β²
β β β
ββββββββββββββ β ββββββββββββββ
β β β
ββββββββ΄ββββββββββββββββ ββββββββββββββββ΄ββββββββββββββ βββββββββββββββββ΄βββββ
β Registrar API β β Subscription Mgmt API β β Authorization API β
β β β β β β
β Calls provider for β β plan_discord_roles table β β discord_roles β
β OAuth operations β β (still owns planβrole β β (still owns role β
β β β mappings) β β metadata) β
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ ββββββββββββββββββββββ
The provider becomes an Anti-Corruption Layer: it translates between Discordβs API and domain-friendly operations. Domains still own their Discord-related data and logic, but they call the provider instead of embedding Discord client code.
Option 3: Thick Provider API
Centralize both Discord access AND Discord-related data:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Discord Provider API β
β β
β β’ Discord API client β
β β’ OAuth token management β
β β’ discord_roles table β
β β’ plan_discord_roles table β
β β’ user_discord_accounts table β
β β’ Role sync logic β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β² β²
β β
ββββββββββββββ ββββββββββββββ
β β
ββββββββ΄ββββββββββββββββ ββββββββββββββββ΄ββββββββββββββ
β Registrar API β β Subscription Mgmt API β
β β β β
β Delegates OAuth to β β Notifies provider of β
β provider β β plan changes β
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
The provider owns everything Discord-related. Domains notify it of relevant events (plan changes, user registration), and the provider handles the rest.
The Decision: Domain-Embedded Integration
I chose Option 1 (domain-embedded) for now, with a planned evolution to Option 2 (extracted provider) once the solution matures.
Why Not a Thick Provider?
Option 3 inverts the natural dependency direction. Domains at the center should call out to the fringe; the fringe should not reach into the center. A provider exists to be used by domains, not to use them.
Role synchronization requires reading a userβs current plans and applying authorization rules. If the Discord provider owns this logic, it must either:
- Call into Subscription Management and Authorization APIs to get the data it needs, which means a fringe concern reaching into the domain center
- Duplicate plan and authorization data into its own storage, creating consistency problems
- Receive all relevant data in the sync request, forcing callers to assemble context that the provider then processes
None of these are clean. The current architecture has Authorization reading from Subscription Management, but thatβs different: both are cross-cutting concern domains at the center. Shared domains exist to be used AND to use each other. A thick Discord provider is a peripheral integration, not a shared domain. It belongs on the fringe, called by domains, not calling into them.
The Authorization API already exists to answer βwhat can this user access?β Adding Discord as another access type fits naturally. Extracting that logic into a Discord provider would fragment authorization decisions across services.
Why Not a Thin Provider (Yet)?
Option 2 is the likely evolution, but creating it now would cost more than it saves.
Current state:
- Single small team
- Limited development time
- One third-party integration (Discord)
- Need for rapid discovery as requirements clarify
What a thin provider adds:
- Another service to deploy and maintain
- API contract to design and version
- Coordination overhead when Discord integration needs change
What a thin provider removes:
- Duplicate Discord client code across domains
- Inconsistent error handling and retry logic
With one integration and one team, the coordination overhead exceeds the benefit of centralization. Each domain can evolve its Discord usage independently without waiting for provider changes.
Creating a provider now would force us to define an interface before understanding what each domain actually needs from Discord. Weβd be designing an abstraction during the period when weβre still discovering requirements. The feature has a limited time window to prove value. If it succeeds, we can spend additional time maturing the architecture. If it fails, weβve avoided building infrastructure for something that didnβt work out.
This calculus changes with team growth. With multiple teams, waiting days or weeks for another team to update a shared API doesnβt solve anyoneβs problem during discovery. But it becomes worthwhile once patterns stabilize and the coordination cost is amortized across many uses.
The Architectural Characteristics Driving This Decision
Two characteristics dominate my priorities for this system:
| Characteristic | Definition | How Embedding Serves It |
|---|---|---|
| Cost | Minimize development and operational expense | No new service to build/deploy/maintain; no coordination overhead; faster time to solution |
| Agility | Ability to respond quickly to changing requirements | Each domain evolves independently; no bottleneck on provider changes; discovery happens in parallel |
A centralized provider optimizes for different characteristics:
| Characteristic | Definition | How Centralization Serves It |
|---|---|---|
| Consistency | Uniform behavior across the system | Single implementation of Discord access; consistent error handling |
| Maintainability | Ease of understanding and modifying | One place to find Discord code; clear boundary |
For a mature system with multiple teams, consistency and maintainability might dominate. For a small team in discovery mode, cost and agility matter more.
What Changes When Conditions Change
This decision isnβt permanent. Several triggers would shift the calculus toward centralization.
| Trigger | Current State | Evolution Signal |
|---|---|---|
| Multiple providers | 1 (Discord only) | 2+ providers with similar patterns |
| Team structure | Single small team | Multiple teams needing Discord integration |
| API call volume | Infrequent (OAuth, role changes) | Real-time sync, frequent operations |
| API complexity | Small, stable subset | Frequent Discord API changes requiring coordinated updates |
Trigger 1: Multiple Third-Party Providers With Similar Patterns
If the company integrates Slack, Telegram, or other community platforms alongside Discord, the domain-embedded approach multiplies:
Current (1 provider):
- Authorization owns discord_roles
- Subscription Mgmt owns plan_discord_roles
- Users owns user_discord_accounts
- 3 domains Γ 1 provider = 3 integration points
Future (3 providers):
- Authorization owns discord_roles, slack_roles, telegram_roles
- Subscription Mgmt owns plan_discord_roles, plan_slack_roles, plan_telegram_roles
- Users owns user_discord_accounts, user_slack_accounts, user_telegram_accounts
- 3 domains Γ 3 providers = 9 integration points
But more providers doesnβt automatically justify extraction. Each provider has its own API, its own concepts, and its own quirks. If providers have genuinely different needs, embedding each separately may still be appropriate. The trigger for extraction isnβt just βmore providersβ but βmore providers with similar enough patterns that a shared abstraction adds value rather than forcing awkward compromises.β
If a second provider emerged with very similar integration patterns, that would be a stronger signal to extract. But even then, the decision would be circumstantial rather than automatic.
Trigger 2: Team Growth
With a single team, coordination overhead is just context switching. With multiple teams, a shared provider creates blocking dependencies:
- Team A owns Authorization and needs a Discord operation to change
- Team B owns the Discord provider
- Team A waits days or weeks for Team B to update the provider API
During discovery, this wait time is unacceptable. The feature has a limited window to prove value, and blocking on another teamβs backlog doesnβt solve the problem that needed to be solved.
But with embedded code, coordination still happens, just through shared values rather than shared code. If teams communicate as they should, they align on conventions for error handling, retry logic, and API usage. Conwayβs Law still applies, but the communication happens through documentation and review rather than through API contracts.
The trigger for extraction isnβt team count alone. Itβs when the coordination cost of maintaining consistent behavior across embeddings exceeds the blocking cost of waiting on a shared provider. That typically happens once patterns stabilize and discovery gives way to steady-state operation.
Trigger 3: Processing Load
Currently, Discord API calls are infrequent: OAuth during registration, role updates on plan changes. If usage patterns shift (real-time presence sync, message integration, frequent role checks), centralized rate limiting and connection pooling become valuable.
Trigger 4: Discord API Complexity
Discordβs API evolves. If changes require coordinated updates across domains, a centralized provider absorbs that complexity. Currently, each domain uses a small, stable subset of the API, so this isnβt pressing.
This Is Intentional Technical Debt
Technical debt is often unintentional: shortcuts taken under pressure that accumulate interest over time. This is different. This is deliberate:
The principal: Duplicate Discord client code across domains; no single place to understand βhow we talk to Discord.β
The interest: When Discord changes their API or we need consistent retry logic, we update multiple places. When debugging Discord issues, we check multiple services.
The payback plan: Once the Discord integration proves itself and matures, consolidate API access into a thin provider. Domains keep their data and logic but call the provider instead of embedding client code. The provider becomes an Anti-Corruption Layer.
Evolution path:
Phase 1 (Current): Domain-Embedded
- Each domain has Discord client code
- Fast to build, easy to change independently
- Interest: duplication across domains
Phase 2 (Future): Thin Provider + Domain Adapters
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Discord Provider API β
β β’ API client, credentials, rate limiting β
β β’ Domain-agnostic operations (assign role, get user, etc.) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β² β² β²
β β β
βββββββββββββββββββ΄βββ ββββββββββββββββ΄βββββββββββ βββββββ΄ββββββββββββββ
β Registrar Adapter β β Subscription Adapter β β Auth Adapter β
β β β β β β
β Domain-specific β β Domain-specific β β Domain-specific β
β Discord logic β β Discord logic β β Discord logic β
ββββββββββββββββββββββ βββββββββββββββββββββββββββ βββββββββββββββββββββ
- Provider handles Discord API concerns
- Adapters translate domain needs to provider operations
- Data stays in domains
- Business logic stays in domains
The adapter pattern preserves domain independence while centralizing infrastructure concerns. Each domainβs adapter can evolve its usage of the provider without affecting others.
Why This Qualifies as True Technical Debt
Most βtechnical debtβ is actually just poor code quality. True technical debt has these properties:
| Property | This Decision |
|---|---|
| Intentional | Yes, I chose this knowing the tradeoffs |
| Understood interest | Yes, I know what maintenance burden it creates |
| Clear payback plan | Yes, evolution to thin provider is defined |
| Rational tradeoff | Yes, short-term agility outweighs long-term maintenance |
The decision isnβt βweβll fix it laterβ with no plan. Itβs βweβll evolve it when these specific triggers occur, in this specific way.β
Consequences and Mitigations
Consequence: Confusion About Where Discord Logic Lives
Without centralization, developers might not know where to look for Discord-related code.
Mitigation: Document the ownership clearly. Discord roles metadata β Authorization. Plan-to-role mappings β Subscription Management. User identity β Users. OAuth orchestration β Registrar. The ADR (Architecture Decision Record) captures this reasoning.
Consequence: Duplicate Discord Client Code
Each domain embeds its own Discord API client.
Mitigation: Share values, not code. The duplication is limited (we use a small API surface), and creating another container just to eliminate copy-paste is often worse than communicating governance and design between tech leads. When evolving to Phase 2, the provider consolidates the client code, but only after we understand what that client actually needs to do.
Consequence: Inconsistent Error Handling
Each domain might handle Discord API errors differently.
Mitigation: Share values, not code. Establish conventions in the ADR: retry logic, timeout configuration, error categorization. Teams share these values through documentation and code review, not through forced code sharing. The goal is consistent behavior through shared understanding, not identical implementations.
Key Lessons
1. Provider vs. Domain Is a Spectrum, Not a Binary
The choice isnβt βextract everythingβ or βembed everything.β Discord API access (infrastructure) can be extracted while Discord business logic (domain) stays embedded in domains. The thin provider + adapter pattern achieves this separation.
2. Architectural Characteristics Should Drive the Decision
Without explicit priorities, architectural debates become opinion battles. When I say βcost and agility matter most for this system,β the domain-embedded approach follows logically. A different system prioritizing consistency and maintainability would choose differently.
3. Intentional Debt Requires a Payback Plan
βWeβll clean it up laterβ isnβt a plan. Intentional debt specifies: what triggers evolution, what the evolved state looks like, and what interest weβre paying until then. Without these, itβs just rationalized shortcuts.
4. Small Teams Have Different Optimal Architectures
Coordination overhead thatβs negligible for a single team becomes significant with multiple teams. The βrightβ architecture depends on whoβs building it, not just whatβs being built.
Small teams can share values without sharing code. Conventions, ADRs, and code review create consistent behavior across embeddings without the blocking dependencies of a shared provider. As teams grow, shared code can evolve from those shared values, not as a substitute for them but as a natural extension. The values remain; the code becomes their embodiment.
5. Integration Discovery Differs from Domain Discovery
Conventional wisdom says monolithic approaches aid discovery in greenfield systems, and thatβs true when youβre still learning where domain boundaries belong. But this isnβt greenfield. The domains are established, each with its own maturity, pace, and team concerns.
The discovery here is different: how does Discord fit into each domainβs existing responsibilities? Each domain needs room to evolve its integration without being blocked by a shared abstraction thatβs also changing. Creating a centralized provider now would force premature abstraction, defining an interface before knowing what each domain actually needs from it. Once integration patterns stabilize across domains, centralization captures what was learned.
Conclusion
Where should third-party integration logic live? The answer depends on what youβre optimizing for and how mature your understanding is.
For a small team in discovery mode with a single third-party integration, embedding within domains minimizes coordination overhead and maximizes agility. The cost is duplication and scattered concerns.
For a mature system with multiple teams and stabilized patterns, a thin provider centralizes infrastructure concerns while domain adapters preserve business logic ownership. The cost is coordination overhead and potential bottlenecks.
I chose embedding now, with a clear path to extraction. This is intentional debt, not neglect. The interest is understood, the payback plan is defined, and the triggers for evolution are explicit. When those triggers occur, the architecture will evolve. Until then, it serves the current needs.
The worst outcome would be building a centralized provider prematurely, paying coordination costs during the period when agility matters most, only to discover the abstraction doesnβt fit the actual usage patterns. Better to let patterns emerge, then capture them.
Find this case study insightful? Share it with your network:
Share on LinkedIn