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. The initial implementation relied on manual verification: employees confirm purchases and assign Discord roles by hand. 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.
I chose to embed Discord concerns within each domain rather than extracting a dedicated Discord API. This is intentional short-term debt: with a single small team and one integration not yet proven, designing a provider interface before knowing what each domain needs from it is premature. The architecture will evolve to a thin provider when specific triggers occur: multiple similar third-party integrations, team growth, or significantly increased API call volume.
The Business Context
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; Discord introduces a third-party API, external identity systems, and platform-specific access controls, which is part of why discovery and agility were weighted 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.
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 β
βββββββββββββββββββββββββ
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: which Discord roles exist, which can be assigned programmatically, and which serve as defaults
- Subscription Management API owns plan-to-role mappings: which roles a given subscription plan grants
- Users API owns the identity mapping between platform users and Discord accounts
- Registrar API has no persistent Discord data; it orchestrates the OAuth flow and delegates storage to the Users API
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), Subscription Management triggers the same authorization sync and roles update automatically.
Embedding client code in each domain doesnβt mean abandoning governance. Credentials and configuration are managed through AWS Parameter Store, ensuring consistent client behavior and centralized credential management. Ownership across domains is documented in an ADR so developers know where Discord concepts live: role metadata in Authorization, plan-to-role mappings in Subscription Management, user identity in Users, OAuth orchestration in Registrar.
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 to get the data it needs (a fringe concern reaching into the domain center), duplicate plan and authorization data into its own storage creating consistency problems, or receive all relevant data in the sync request and force callers to assemble context it then processes. None of these is clean.
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. A new service needs deployment and maintenance infrastructure, and more critically, an interface designed before knowing what each domain actually needs from it. With one integration not yet proven, that investment isnβt justified.
The feature has a limited time window to prove value. If it succeeds, we can invest in maturing the architecture. If it fails, weβve avoided building infrastructure for something that didnβt work out.
This decision trades off two priorities against two others:
| Priority | Embedding | Thin Provider |
|---|---|---|
| Cost | Lower: no new service to build or operate | Higher: deploy, maintain, version an API contract |
| Agility | Higher: each domain evolves independently | Lower: interface changes require updating the provider first |
| Consistency | Lower: duplicate client behavior across domains | Higher: single implementation |
| Maintainability | Lower: Discord code is scattered | Higher: one place to find it |
For a small team prioritizing speed and flexibility, the top two rows dominate. For a mature system with stabilized patterns and multiple teams, the bottom two would.
When to Centralize
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: three domains times three providers becomes nine integration points instead of three. More providers donβt automatically justify extraction; the trigger is providers with similar enough patterns that a shared abstraction adds value rather than forcing awkward compromises.
Trigger 2: Team Growth
With a single team, maintaining consistent behavior across embeddings is a documentation and code review problem. With multiple teams, it becomes a coordination problem: teams need to align on conventions they didnβt write and canβt easily enforce. A shared provider makes those conventions structural. The trigger isnβt team count alone; itβs when coordinating consistent behavior across embeddings costs more than maintaining a shared interface.
Trigger 3: Processing Load
Currently, Discord API calls are infrequent: OAuth during registration, role updates on plan changes. If usage patterns shift toward real-time presence sync, message integration, or frequent role checks, centralized rate limiting and connection pooling become valuable.
Trigger 4: Discord API Complexity
If Discord API changes require coordinated updates across domains, a centralized provider absorbs that complexity in one place. 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. The principal is known: duplicate Discord client code across domains with no single place to understand βhow we talk to Discord.β The interest is also known: 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 is specific. Once the Discord integration proves itself and matures, consolidate API access into a thin provider. Domains keep their data and business logic but call the provider for Discord API access instead of embedding client code directly.
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.
Key Lessons
Extract infrastructure; keep business logic in the domain that owns it
The choice isnβt βextract everythingβ or βembed everything.β Discord API access (infrastructure) can be extracted while Discord business logic (domain) stays embedded. The thin provider plus adapter pattern achieves this separation.
Name your architectural priorities before the debate starts
Without explicit priorities, architectural debates become opinion battles. When cost and agility are the dominant characteristics, the domain-embedded approach follows logically; a system prioritizing consistency and maintainability would choose differently. Naming the characteristics forces the tradeoff into the open rather than leaving it implicit.
Intentional debt needs a named payback trigger, not a vague intention
Most βtechnical debtβ is actually just poor code quality. True technical debt is intentional, with understood interest and a clear 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.
Share values, not code, until the team outgrows it
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. A single team can achieve consistent behavior by sharing values rather than code: conventions, ADRs, and code review enforce the same standards that a shared service would otherwise impose. As teams grow, shared code can evolve from those shared values rather than replacing them.
Donβt extract until patterns have stabilized across domains
The discovery here isnβt where domain boundaries belong; those are established. Itβs how Discord fits 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. Once integration patterns stabilize, centralization captures what was learned rather than defining it prematurely.
Find this case study insightful? Share it with your network:
Share on LinkedIn