thin-adapters

star 0

Thin adapters pattern - connectors contain zero business logic. Use when adding platform integrations or refactoring boundaries.

izgorodin By izgorodin schedule Updated 1/22/2026

name: thin-adapters description: Thin adapters pattern - connectors contain zero business logic. Use when adding platform integrations or refactoring boundaries. allowed-tools: Read Write Edit Grep

Thin Adapters Architecture

Adapters translate. Core decides.

The Rule

┌─────────────────────────────────────────────────────┐
│                    ADAPTER                          │
│  • Parse platform format                            │
│  • Translate to/from canonical models               │
│  • Handle platform-specific quirks                  │
│  • NO business logic                                │
│  • NO decisions about what to do                    │
└─────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────┐
│                     CORE                            │
│  • All business rules                               │
│  • All decisions                                    │
│  • Platform-agnostic                                │
│  • Testable without adapters                        │
└─────────────────────────────────────────────────────┘

Inbound Adapter

ONLY translates external → canonical:

# src/connectors/telegram/inbound.py

async def normalize_inbound(payload: dict) -> NormalizedEvent | None:
    """
    Translate Telegram payload to NormalizedEvent.

    Returns None if payload is not a user message.
    Does NOT decide whether to respond.
    Does NOT check for time mentions.
    Does NOT look up user timezone.
    """
    message = payload.get("message")
    if not message or "text" not in message:
        return None  # Not a text message

    return NormalizedEvent(
        platform=Platform.TELEGRAM,
        platform_event_id=f"{message['chat']['id']}_{message['message_id']}",
        chat_id=str(message["chat"]["id"]),
        user_id=str(message["from"]["id"]),
        text=message["text"],
        timestamp=datetime.fromtimestamp(message["date"]),
        raw_payload=payload
    )

Outbound Adapter

ONLY translates canonical → external:

# src/connectors/telegram/outbound.py

async def send_outbound(msg: OutboundMessage) -> bool:
    """
    Send OutboundMessage via Telegram API.

    Does NOT decide what to send.
    Does NOT format the message.
    Does NOT handle retries (that's infrastructure).
    """
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
            json={
                "chat_id": msg.chat_id,
                "text": msg.text,
                "parse_mode": "HTML"
            }
        )
        return response.status_code == 200

What Goes in Core

Everything else:

# src/core/handler.py

async def handle_message(event: NormalizedEvent) -> OutboundMessage | None:
    """
    Core business logic. Platform-agnostic.

    THIS is where decisions happen:
    - Should we respond?
    - Is this a duplicate?
    - Does it contain time?
    - What timezone to use?
    - How to format response?
    """
    # Dedupe check
    if await is_duplicate(event):
        return None

    # Time detection
    parsed_time = await parse_time(event.text)
    if not parsed_time:
        return None

    # Timezone resolution
    user_tz = await get_user_timezone(event.user_id, event.platform)
    if user_tz.confidence < THRESHOLD:
        return prompt_verification(event)

    # Conversion
    conversions = convert_to_team_zones(parsed_time, user_tz)

    # Format response
    return OutboundMessage(
        platform=event.platform,
        chat_id=event.chat_id,
        text=format_conversions(conversions)
    )

Testing Benefits

Core tests don't need adapters:

async def test_handler_skips_duplicate():
    event = NormalizedEvent(...)  # No Telegram needed
    # Mock only dedupe, not platform
    with patch("is_duplicate", return_value=True):
        result = await handle_message(event)
    assert result is None

Adapter tests don't need core:

async def test_telegram_normalize():
    payload = TELEGRAM_FIXTURE
    event = await normalize_inbound(payload)
    assert event.platform == Platform.TELEGRAM
    # No business logic tested here

Anti-Patterns

DON'T: Business logic in adapter

# BAD: Adapter making decisions
async def normalize_inbound(payload: dict) -> NormalizedEvent | None:
    message = payload.get("message")
    if not message:
        return None

    # NO! This is business logic
    if "time" not in message["text"].lower():
        return None  # Don't process non-time messages

    # NO! This is business logic
    if await is_user_banned(message["from"]["id"]):
        return None

DON'T: Platform-specific code in core

# BAD: Core knows about Telegram
async def handle_message(event: NormalizedEvent):
    if event.platform == Platform.TELEGRAM:
        # NO! Platform-specific handling
        await telegram_specific_thing()

DON'T: Formatting in adapter

# BAD: Adapter formatting response
async def send_outbound(msg: OutboundMessage) -> bool:
    # NO! Core should format
    text = f"🕐 {msg.time} in your timezone"

Directory Structure

src/
├── core/                    # Platform-agnostic
│   ├── handler.py          # Business logic
│   ├── models.py           # Canonical models
│   └── ...
└── connectors/             # Platform-specific
    ├── telegram/
    │   ├── inbound.py      # Telegram → NormalizedEvent
    │   └── outbound.py     # OutboundMessage → Telegram
    ├── discord/
    │   ├── inbound.py
    │   └── outbound.py
    └── whatsapp/
        ├── inbound.py
        └── outbound.py

The Test

If you can swap adapters without touching core, you did it right.

Discord, WhatsApp, Slack, email - core doesn't care.

Install via CLI
npx skills add https://github.com/izgorodin/team-ops-assistant --skill thin-adapters
Repository Details
star Stars 0
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator