Agnostic Core, Specific Edges

Your core business logic should be infrastructure-agnostic. The "edges" of your application handle specifics.

The Pattern

┌──────────────────────────────────────────────────────────────┐
│                         EDGES (IN)                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐       │
│  │ HTTP Handler │  │  Pub/Sub     │  │  CLI Command │       │
│  │              │  │  Subscriber  │  │              │       │
│  │ JSON →       │  │ message →    │  │ args →       │       │
│  │ domain       │  │ domain       │  │ domain       │       │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘       │
│         │                 │                 │               │
│         ▼                 ▼                 ▼               │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                     CORE                             │   │
│  │                                                      │   │
│  │              Business Logic                          │   │
│  │         (speaks only domain language)                │   │
│  │                                                      │   │
│  └───────┬──────────────────────────────┬───────────┘   │
│          │                              │               │
│          ▼                              ▼               │
│  ┌──────────────┐              ┌──────────────┐         │
│  │  Database    │              │  API Client  │         │
│  │  Repository  │              │              │         │
│  │              │              │  string →    │         │
│  │  SQL →       │              │  boolean     │         │
│  │  domain      │              │              │         │
│  └──────────────┘              └──────────────┘         │
│                        EDGES (OUT)                       │
└──────────────────────────────────────────────────────────┘

Example: Repository Pattern

Edges handle infrastructure. The service owns business logic — both orchestration and transformation.

# repository.py - EDGE (database-specific)
class UserRepository:
    def find_by_id(self, id):
        row = db.query("SELECT * FROM users WHERE id = %s", [id])
        return self._to_domain(row) if row else None

    def save(self, user):
        db.execute("UPDATE users SET status = %s WHERE id = %s",
            ["A" if user.is_active else "I", user.id])

    def _to_domain(self, row):
        return User(
            id=row.id,
            name=row.full_name,            # DB column name → domain name
            is_active=row.status == "A",    # DB code → boolean
        )

# service.py - CORE (business logic)
class UserService:
    def __init__(self, user_repo):
        self.user_repo = user_repo

    def deactivate(self, user_id):
        user = self.user_repo.find_by_id(user_id)
        if not user:           raise NotFoundError()
        if not user.is_active: return user
        updated = User(**user, is_active=False)
        self.user_repo.save(updated)
        return updated

# handler.py - EDGE (protocol translation)
@route("POST /users/:id/deactivate")
def deactivate_user(request):
    try:
        user = user_service.deactivate(request.params.id)
        return 200, user
    except NotFoundError:
        return 404

Translate at Boundaries

External data formats should be translated at the edge, not in business code.

# api_client.py - EDGE (incoming)
class ExternalApiClient:
    def get_feature_flags(self):
        response = fetch("/api/flags")
        data = response.json()

        # External API sends strings, we use booleans
        return FeatureFlags(
            dark_mode=data["dark_mode"] == "true",
            beta_features=data["beta"] == "1",
        )

# feature_service.py - CORE
class FeatureService:
    # Never sees strings - only typed booleans
    def is_enabled(self, flags, feature):
        return getattr(flags, feature)

Backing Services as Attached Resources

Databases, message queues, caches, external APIs — treat them all the same way. They're resources your app attaches to via configuration.

┌─────────────┐
│    App      │
└──┬──┬──┬──┬─┘
   │  │  │  │
   ▼  ▼  ▼  ▼
  DB MQ S3 API

  All accessed via URL/credentials in config

The principle: Swap any service by changing config, no code changes.

# Switch from local to managed database
DATABASE_URL=postgres://localhost/myapp          # local
DATABASE_URL=postgres://user:pass@rds.aws/myapp  # production

This is agnostic core applied to infrastructure: your app doesn't know or care whether its database is local, managed, or in another continent.

Why This Matters

  • Testability: Core logic can be tested without databases, APIs, or external systems
  • Flexibility: Swap PostgreSQL for MongoDB? Only change the repository
  • Portability: Swap any backing service by changing config alone
  • Clarity: Business rules aren't buried in data transformation code
  • Maintenance: External API changes don't ripple through your codebase

Draw Consistency Boundaries

Identify the smallest groups of things that must change together, and enforce rules at those boundaries. Not everything in the system needs to be consistent with everything else — only things within the same boundary do.

Scope Your Models

A single unified model breaks at scale. Give each bounded context its own model. The same concept (e.g. a "user") can and should look different depending on which part of the system is working with it.

External Dependencies Are Only Up Half the Time

Failures from external services are the normal path, not edge cases. Design for degraded states, timeouts, and partial failures as first-class concerns rather than exceptional ones.