| Proven | Emerging | Experimental | Deprecated |
Enterprise customers often have standardized toolchains and don’t want to change their issue tracker, chat platform, or other tools just to use a new product. This creates several challenges:
Traditional approaches either:
This pattern enables plug-and-play integration swapping with manageable complexity.
Ted Nadeau’s insight: “Many-to-One-to-Many Pattern”
Business Logic (many call sites)
↓
Integration Router (one abstraction) ← Swap happens here
↓
External API / MCP Adapter (many providers)
Change provider by swapping the router implementation, not changing business logic.
The Integration Swappability Pattern uses a Router layer to abstract external integrations. Business logic depends on the Router interface, not specific provider implementations. This enables swapping providers (GitHub ↔ Jira, Slack ↔ Teams, OpenAI ↔ Gemini) by changing configuration or dependency injection, not code.
Router Layer = Abstract interface + pluggable implementations
# Business logic depends on interface
class IssueRouter(ABC):
@abstractmethod
async def create_issue(self, title: str, body: str) -> Issue:
pass
# Implementations are swappable
class GitHubRouter(IssueRouter):
async def create_issue(self, title: str, body: str) -> Issue:
# GitHub-specific implementation
pass
class JiraRouter(IssueRouter):
async def create_issue(self, title: str, body: str) -> Issue:
# Jira-specific implementation
pass
# Business logic uses router, doesn't care which
router: IssueRouter = get_configured_router() # GitHub or Jira
issue = await router.create_issue("Bug report", "Details...")
services/integrations/
├── base_router.py # Abstract router interface
├── github/
│ ├── github_router.py # GitHub implementation
│ └── mcp_adapter.py # MCP protocol adapter
├── jira/
│ ├── jira_router.py # Jira implementation
│ └── rest_adapter.py # REST API adapter
└── factory.py # Router factory (config-based)
# services/integrations/base_router.py
from abc import ABC, abstractmethod
from typing import Optional, List
from dataclasses import dataclass
@dataclass
class Issue:
"""Provider-agnostic issue model."""
id: str
title: str
body: str
url: str
status: str
class IssueTrackerRouter(ABC):
"""Abstract router for issue tracker integrations."""
@abstractmethod
async def create_issue(
self,
title: str,
body: str,
labels: Optional[List[str]] = None
) -> Issue:
"""Create issue in tracker. Returns provider-agnostic Issue."""
pass
@abstractmethod
async def get_issue(self, issue_id: str) -> Optional[Issue]:
"""Retrieve issue by ID."""
pass
@abstractmethod
async def update_issue(self, issue_id: str, **updates) -> Issue:
"""Update issue with new data."""
pass
# services/integrations/github/github_router.py
class GitHubIssueRouter(IssueTrackerRouter):
"""GitHub implementation of issue tracker router."""
def __init__(self, mcp_client: MCPClient):
self.mcp = mcp_client
async def create_issue(
self,
title: str,
body: str,
labels: Optional[List[str]] = None
) -> Issue:
# Use MCP protocol to create GitHub issue
gh_issue = await self.mcp.issues.create(
title=title,
body=body,
labels=labels or []
)
# Convert to provider-agnostic model
return Issue(
id=gh_issue["node_id"],
title=gh_issue["title"],
body=gh_issue["body"],
url=gh_issue["html_url"],
status=gh_issue["state"]
)
# services/integrations/jira/jira_router.py
class JiraIssueRouter(IssueTrackerRouter):
"""Jira implementation of issue tracker router."""
def __init__(self, jira_client: JiraRESTClient):
self.jira = jira_client
async def create_issue(
self,
title: str,
body: str,
labels: Optional[List[str]] = None
) -> Issue:
# Use Jira REST API
jira_issue = await self.jira.create(
project="PM",
issue_type="Task",
summary=title,
description=body,
labels=labels or []
)
# Convert to provider-agnostic model
return Issue(
id=jira_issue["key"],
title=jira_issue["fields"]["summary"],
body=jira_issue["fields"]["description"],
url=jira_issue["self"],
status=jira_issue["fields"]["status"]["name"]
)
# services/integrations/factory.py
def get_issue_tracker_router() -> IssueTrackerRouter:
"""Factory: Returns configured issue tracker router."""
tracker_type = os.getenv("ISSUE_TRACKER", "github")
if tracker_type == "github":
mcp_client = get_mcp_client("github")
return GitHubIssueRouter(mcp_client)
elif tracker_type == "jira":
jira_client = get_jira_client()
return JiraIssueRouter(jira_client)
else:
raise ValueError(f"Unsupported tracker: {tracker_type}")
# Business logic (services/intent_service/handlers.py)
async def handle_create_issue(title: str, body: str):
"""Business logic doesn't know about GitHub or Jira."""
router = get_issue_tracker_router() # Gets GitHub or Jira
issue = await router.create_issue(title, body)
return {"issue_id": issue.id, "url": issue.url}
# .env or config file
ISSUE_TRACKER=github # or jira, linear, gitlab
# GitHub config (if using GitHub)
GITHUB_TOKEN=ghp_...
GITHUB_REPO=owner/repo
# Jira config (if using Jira)
JIRA_URL=https://company.atlassian.net
JIRA_TOKEN=...
JIRA_PROJECT=PM
primary_router → fallback_router for resilienceIssue Trackers:
services/integrations/github/github_router.py - GitHub implementation (currently active)services/integrations/jira/jira_router.py (when enterprise customers need it)services/integrations/linear/linear_router.py (when requested)Chat Platforms:
services/integrations/slack/slack_router.py - Slack implementation (currently active)services/integrations/teams/teams_router.py (for Microsoft shops)services/integrations/discord/discord_router.py (for developer communities)LLM Providers:
services/llm/clients.py - Abstraction existsCalendar:
services/integrations/calendar/ - MCP adapter patternDocs/Wiki:
services/integrations/notion/notion_router.py - Notion implementation| Integration | Current | Alternatives Ready | Effort to Add | Enterprise Value |
|---|---|---|---|---|
| Issue Tracker | GitHub | Jira, Linear, GitLab | Medium | High |
| Chat | Slack | Teams, Discord | Medium | High |
| LLM | Anthropic/OpenAI | Gemini, Cohere, local | Low-Medium | Medium |
| Calendar | Outlook, iCal | Medium | Medium | |
| Docs | Notion | Confluence, Google Docs | Medium | Medium |
| Database | PostgreSQL | MS-SQL, MySQL | High | Medium |
# tests/unit/services/integrations/test_issue_routers.py
class MockIssueRouter(IssueTrackerRouter):
"""Mock router for testing without real API calls."""
async def create_issue(self, title: str, body: str, labels=None):
return Issue(
id="mock-123",
title=title,
body=body,
url="https://mock.example.com/issue/123",
status="open"
)
# Test business logic with mock router
@pytest.mark.asyncio
async def test_create_issue_handler_with_mock():
router = MockIssueRouter()
issue = await router.create_issue("Test", "Body")
assert issue.id == "mock-123"
assert issue.title == "Test"
Before (tightly coupled):
# Business logic directly calls GitHub API
gh_client = GitHubClient(token)
issue = gh_client.create_issue(title, body)
# Now locked to GitHub!
After (swappable):
# Business logic uses router
router = get_issue_tracker_router() # Config determines GitHub/Jira
issue = await router.create_issue(title, body)
# Can swap to Jira by changing config!
Ted’s insight:
“External library calls should go through abstraction layers (many-to-one-to-many pattern)” “Example: GitHub API abstraction enables swapping to Jira, monitoring, metering”
This pattern implements exactly what Ted described. Our Router pattern is his many-to-one-to-many pattern:
Ted’s validation: “You’re describing what we already built!” (Router pattern)
dev/2025/11/20/ted-nadeau-follow-up-research.md (Section 3)Document which features work on each provider:
| Feature | GitHub | Jira | Linear | Notes |
|---|---|---|---|---|
| Create issue | ✅ | 🔜 | 🔜 | Core functionality |
| Add comment | ✅ | 🔜 | 🔜 | |
| Update status | ✅ | 🔜 | 🔜 | |
| Assign user | ✅ | 🔜 | 🔜 | User mapping needed |
| Attach files | ✅ | 🔜 | 🔜 | |
| Link issues | ✅ | 🔜 | 🔜 | |
| Custom fields | ❌ | 🔜 | 🔜 | Jira-specific |
| Spatial intelligence | ✅ | ❌ | ❌ | Piper-specific |
| Legend: ✅ Implemented | 🔜 Planned | ❌ Not supported |
Pattern created: November 20, 2025 Author: Code Agent (Research Session) Inspired by: Ted Nadeau’s inter-operability questions Status: Proven (already in production) Validated by: Ted Nadeau (external architecture review)