Skip to content

Subdomain Resolver

Subdomain-based Tenant Resolver Module

This module implements tenant resolution based on subdomains.

Subdomain Resolution

Each tenant is identified by a unique subdomain. The tenant_id is derived directly from the subdomain portion of the request host.

Example: - "acme.example.com" → Tenant "acme" - "globex.example.com" → Tenant "globex" - "acme-prod.example.com" → Tenant "acme-prod"

Purpose

Provides the simplest and most common multi-tenancy pattern: - Tenant identification embedded in URL - Direct mapping from subdomain to tenant_id - No external lookup tables required - Tenant_id directly matches subdomain - Lightweight and performant

Subdomain Extraction

The subdomain is extracted from the host header:

subdomain = request.get_host().split(".")[0]

Examples: - "acme.example.com" → "acme" - "globex.example.com" → "globex" - "api.acme.example.com" → "api" (treats first part as subdomain) - "example.com" → "example" (edge case - takes first part)

Process: 1. Get host from HTTP Host header (e.g., "acme.example.com") 2. Split on "." (dot delimiter) 3. Take first element (index [0]) 4. Treat as tenant_id

Tenant Lookup

The extracted subdomain is used as tenant_id to lookup Tenant:

Tenant.objects.get(tenant_id=subdomain)

Database query details: - Query Tenant model - Match on tenant_id field - tenant_id is usually unique, indexed - Fast lookup (O(1) with index) - Returns Tenant object if exists

Comparison with CustomDomainTenantResolver:

SubdomainTenantResolver:
- Tenant_id from subdomain directly
- Pattern: "tenant_id.example.com"
- No additional database lookups needed
- Fast single-query resolution
- Tenant_id must match subdomain

CustomDomainTenantResolver:
- Tenant from custom domain mapping
- Pattern: Arbitrary custom domains
- Requires Domain model lookup
- Additional database query
- Multiple domains per tenant possible
Error Handling

If subdomain doesn't correspond to valid tenant_id: - Raises TenantNotFound exception - Middleware catches and handles (typically 404) - Never returns None (explicit error) - Subdomain must exist as tenant_id

Configuration

Use in Django settings:

OMNITENANT_CONFIG = {
    'TENANT_RESOLVER': 'django_omnitenant.resolvers.SubdomainTenantResolver',
}

Alternatively, configure base domain:

OMNITENANT_CONFIG = {
    'TENANT_RESOLVER': 'django_omnitenant.resolvers.SubdomainTenantResolver',
    'BASE_DOMAIN': 'example.com',  # For validation
}

Usage Example
# Create tenants
from django_omnitenant.models import Tenant

tenant1 = Tenant.objects.create(tenant_id='acme')
tenant2 = Tenant.objects.create(tenant_id='globex')

# Requests resolve:
# GET http://acme.example.com → tenant_id='acme'
# GET http://globex.example.com → tenant_id='globex'
# GET http://example.com → TenantNotFound (no subdomain)
# GET http://unknown.example.com → TenantNotFound (tenant doesn't exist)
Tenant ID Conventions

Good practices for tenant_id: - Lowercase letters: "acme", "globex" - Hyphens for multi-word: "acme-corp", "globex-inc" - Numbers for versions: "acme-prod", "acme-staging" - No underscores or spaces (invalid in subdomains) - Keep short and memorable - RFC 1123 compliant (DNS name rules)

Performance
  • Direct Tenant model lookup
  • Single database query
  • tenant_id is usually indexed
  • O(1) lookup time
  • No join or relationship traversal needed
  • Faster than custom domain resolution
Caching Strategy

Subdomain-to-tenant mappings can be cached:

cache_key = f'subdomain_tenant:{subdomain}'
tenant = cache.get(cache_key)
if not tenant:
    try:
        tenant = Tenant.objects.get(tenant_id=subdomain)
        cache.set(cache_key, tenant, timeout=3600)
        return tenant
    except Tenant.DoesNotExist:
        raise TenantNotFound
return tenant
Edge Cases

Root domain (no subdomain): - "example.com" → subdomain = "example" - May not correspond to tenant_id - Usually raises TenantNotFound - Solution: Configure fallback or default tenant

Multiple subdomains: - "api.acme.example.com" → subdomain = "api" - Only first part used - "api" must exist as tenant_id - Solution: Create tenant with tenant_id="api" or use different resolver

Port in host: - "acme.example.com:8000" → split still works - "acme" extracted correctly (port added separately) - request.get_host() includes port, split(".")[0] unaffected

SubdomainTenantResolver

Bases: BaseTenantResolver

Resolver that identifies tenants by subdomain.

This resolver extracts the subdomain portion of the request host and uses it directly as the tenant_id to look up the corresponding Tenant.

This is the simplest and most common multi-tenancy pattern where each tenant is assigned a unique subdomain that maps directly to their tenant_id.

Key Features
  • Direct tenant_id extraction from subdomain
  • No additional lookup tables or models needed
  • Fast single-query lookup
  • Tenant_id must match subdomain exactly
  • Standard multi-tenancy pattern
  • Thread-safe and request-safe
Subdomain Extraction

The subdomain is the first component of the domain name:

Examples: - "acme.example.com" → "acme" - "globex.example.com" → "globex" - "api.acme.example.com" → "api" (only first part used) - "example.com" → "example" (no subdomain)

Process: 1. Get HTTP Host header (e.g., "acme.example.com:8000") 2. Split on "." (dots) 3. Take first element 4. Use as tenant_id

Tenant Lookup

Extracted subdomain is queried against Tenant model: - tenant_id = subdomain - Single database query - Exact match lookup - Returns Tenant if found - Raises TenantNotFound if not found

Performance
  • Single database query (no joins)
  • tenant_id field typically indexed
  • O(1) lookup time
  • No relationship traversal
  • Faster than custom domain resolution
Source code in django_omnitenant/resolvers/subdomain_resolver.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
class SubdomainTenantResolver(BaseTenantResolver):
    """
    Resolver that identifies tenants by subdomain.

    This resolver extracts the subdomain portion of the request host and
    uses it directly as the tenant_id to look up the corresponding Tenant.

    This is the simplest and most common multi-tenancy pattern where each
    tenant is assigned a unique subdomain that maps directly to their
    tenant_id.

    Key Features:
        - Direct tenant_id extraction from subdomain
        - No additional lookup tables or models needed
        - Fast single-query lookup
        - Tenant_id must match subdomain exactly
        - Standard multi-tenancy pattern
        - Thread-safe and request-safe

    Subdomain Extraction:
        The subdomain is the first component of the domain name:

        Examples:
        - "acme.example.com" → "acme"
        - "globex.example.com" → "globex"
        - "api.acme.example.com" → "api" (only first part used)
        - "example.com" → "example" (no subdomain)

        Process:
        1. Get HTTP Host header (e.g., "acme.example.com:8000")
        2. Split on "." (dots)
        3. Take first element
        4. Use as tenant_id

    Tenant Lookup:
        Extracted subdomain is queried against Tenant model:
        - tenant_id = subdomain
        - Single database query
        - Exact match lookup
        - Returns Tenant if found
        - Raises TenantNotFound if not found

    Performance:
        - Single database query (no joins)
        - tenant_id field typically indexed
        - O(1) lookup time
        - No relationship traversal
        - Faster than custom domain resolution

    Attributes:
        None (stateless, no configuration needed)
    """

    def resolve(self, request) -> object | None:
        """
        Resolve tenant from subdomain in request.

        Extracts the subdomain portion of the request host and uses it
        directly as the tenant_id to lookup the corresponding Tenant.

        Args:
            request (django.http.HttpRequest): The HTTP request
                                              Contains host/domain information

        Returns:
            BaseTenant: The tenant matching the subdomain

        Raises:
            TenantNotFound: If no tenant exists with tenant_id matching subdomain

        Process:
            1. Extract subdomain from request host
            2. Query Tenant model for matching tenant_id
            3. Return tenant if found
            4. Raise TenantNotFound if not found

        Subdomain Extraction:
            Subdomain is extracted by splitting host on dots:

            ```python
            subdomain = request.get_host().split(".")[0]
            ```

            request.get_host() returns the HTTP Host header:
            - Includes port if present: "acme.example.com:8000"
            - No port: "acme.example.com"
            - Full FQDN: "acme.example.com"

            Split on "." takes first component:
            - "acme.example.com" → ["acme", "example", "com"] → "acme"
            - "acme.example.com:8000" → "acme.example.com" → ["acme", ...] → "acme"
            - "example.com" → ["example", "com"] → "example"

            Note: Port is NOT included in the domain part (handled by split):
            ```python
            # request.get_host() = "acme.example.com:8000"
            host_without_port = "acme.example.com"
            subdomain = "acme"
            ```

        Examples of subdomain extraction:
            ```
            Host: "acme.example.com" → "acme"
            Host: "globex.example.com" → "globex"
            Host: "api.acme.example.com" → "api" (only first part)
            Host: "example.com" → "example" (edge case)
            Host: "acme.example.com:8000" → "acme" (port ignored)
            Host: "my-tenant.example.com" → "my-tenant" (hyphens allowed)
            ```

        Tenant Model Query:
            Queries the Tenant model:

            ```python
            Tenant = get_tenant_model()  # Get configured Tenant model
            return Tenant.objects.get(tenant_id=subdomain)
            ```

            The get_tenant_model() utility:
            - Returns configured Tenant model
            - Respects custom Tenant implementations
            - Loads from app registry
            - Default: BaseTenant

            Query details:
            - Filters Tenant on tenant_id field
            - tenant_id is usually unique, indexed
            - Single query execution
            - Returns Tenant object

        Error Handling:
            If no Tenant exists with matching tenant_id:

            ```python
            try:
                return Tenant.objects.get(tenant_id=subdomain)
            except Tenant.DoesNotExist:
                raise TenantNotFound
            ```

            Raises TenantNotFound when:
            - Subdomain doesn't match any tenant_id
            - Typo in subdomain
            - Tenant deleted but requests continue
            - Subdomain points to root domain

            Not found cases:
            - "unknown.example.com" → no tenant_id="unknown"
            - "api.example.com" → no tenant_id="api"
            - "example.com" → may be no tenant_id="example"

        Examples:

            Successful resolution:
            ```python
            # Tenant with tenant_id='acme' exists
            request = RequestFactory().get('/')
            request.META['HTTP_HOST'] = 'acme.example.com'

            resolver = SubdomainTenantResolver()
            tenant = resolver.resolve(request)
            # Returns: Tenant(tenant_id='acme')
            ```

            Different subdomain:
            ```python
            request.META['HTTP_HOST'] = 'globex.example.com'
            # Subdomain 'globex' extracted
            # Tenant with tenant_id='globex' returned
            ```

            With port number:
            ```python
            request.META['HTTP_HOST'] = 'acme.example.com:8000'
            # Subdomain 'acme' extracted (port not included)
            # Tenant with tenant_id='acme' returned
            ```

            Tenant not found:
            ```python
            request.META['HTTP_HOST'] = 'unknown.example.com'
            # Subdomain 'unknown' extracted
            # No Tenant with tenant_id='unknown'
            # Raises: TenantNotFound
            ```

            Root domain:
            ```python
            request.META['HTTP_HOST'] = 'example.com'
            # Subdomain 'example' extracted (only first part)
            # Usually no Tenant with tenant_id='example'
            # Raises: TenantNotFound
            # Solution: Configure default tenant or different resolver
            ```

        Performance Characteristics:
            - Single database query (SELECT * FROM tenant WHERE tenant_id=...)
            - No joins or relationships traversed
            - tenant_id field usually indexed
            - O(1) lookup time
            - Typically <1ms database time
            - Fastest resolver option

        Comparison with CustomDomainTenantResolver:
            SubdomainTenantResolver:
            - Queries Tenant model directly
            - 1 database query per request
            - No custom domain table needed
            - Subdomain must equal tenant_id

            CustomDomainTenantResolver:
            - Queries Domain model first, then gets tenant
            - 2 database queries per request
            - Requires Domain model with tenant FK
            - Multiple domains per tenant possible

        Caching Strategy:
            For high-traffic applications, caching helps:

            ```python
            from django.core.cache import cache

            cache_key = f'subdomain_tenant:{subdomain}'
            tenant = cache.get(cache_key)
            if tenant:
                return tenant

            try:
                tenant = Tenant.objects.get(tenant_id=subdomain)
                # Cache for 1 hour
                cache.set(cache_key, tenant, timeout=3600)
                return tenant
            except Tenant.DoesNotExist:
                raise TenantNotFound
            ```

            Benefits:
            - Avoids database query on cache hit
            - Cache hit ratio typically 95%+
            - Minimal overhead (cache lookup vs DB)
            - Suitable for high-concurrency

        Testing:
            ```python
            from django.test import TestCase
            from django.test.client import RequestFactory

            class TestSubdomainResolver(TestCase):
                def setUp(self):
                    self.factory = RequestFactory()
                    self.tenant = Tenant.objects.create(tenant_id='acme')

                def test_resolve_subdomain(self):
                    request = self.factory.get('/')
                    request.META['HTTP_HOST'] = 'acme.example.com'

                    resolver = SubdomainTenantResolver()
                    tenant = resolver.resolve(request)

                    assert tenant == self.tenant

                def test_resolve_with_port(self):
                    request = self.factory.get('/')
                    request.META['HTTP_HOST'] = 'acme.example.com:8000'

                    resolver = SubdomainTenantResolver()
                    tenant = resolver.resolve(request)

                    assert tenant == self.tenant

                def test_resolve_unknown_subdomain(self):
                    request = self.factory.get('/')
                    request.META['HTTP_HOST'] = 'unknown.example.com'

                    resolver = SubdomainTenantResolver()

                    with pytest.raises(TenantNotFound):
                        resolver.resolve(request)

                def test_resolve_hyphenated_subdomain(self):
                    tenant = Tenant.objects.create(tenant_id='acme-prod')
                    request = self.factory.get('/')
                    request.META['HTTP_HOST'] = 'acme-prod.example.com'

                    resolver = SubdomainTenantResolver()
                    resolved_tenant = resolver.resolve(request)

                    assert resolved_tenant == tenant
            ```

        See Also:
            - base.py: Abstract resolver interface
            - customdomain_resolver.py: Custom domain-based alternative
            - models.py: BaseTenant model
            - middleware.py: Uses resolver for routing
            - exceptions.py: TenantNotFound exception
        """
        # Extract subdomain from request host
        # request.get_host() returns HTTP Host header (e.g., "acme.example.com" or "acme.example.com:8000")
        # Split on "." to separate domain components
        # Take first element [0] which is the subdomain
        subdomain = request.get_host().split(".")[0]

        # Get the Tenant model (respects custom implementations via get_tenant_model utility)
        Tenant = get_tenant_model()

        try:
            # Query Tenant model for a tenant with matching tenant_id
            # Direct lookup - no joins or relationships
            # Returns Tenant object if found
            return Tenant.objects.get(tenant_id=subdomain)
        except Tenant.DoesNotExist:
            # No tenant exists with this tenant_id
            # Raise TenantNotFound exception (not None)
            # Middleware will catch and handle (typically 404)
            raise TenantNotFound

resolve(request)

Resolve tenant from subdomain in request.

Extracts the subdomain portion of the request host and uses it directly as the tenant_id to lookup the corresponding Tenant.

Parameters:

Name Type Description Default

request

HttpRequest

The HTTP request Contains host/domain information

required

Returns:

Name Type Description
BaseTenant object | None

The tenant matching the subdomain

Raises:

Type Description
TenantNotFound

If no tenant exists with tenant_id matching subdomain

Process
  1. Extract subdomain from request host
  2. Query Tenant model for matching tenant_id
  3. Return tenant if found
  4. Raise TenantNotFound if not found
Subdomain Extraction

Subdomain is extracted by splitting host on dots:

subdomain = request.get_host().split(".")[0]

request.get_host() returns the HTTP Host header: - Includes port if present: "acme.example.com:8000" - No port: "acme.example.com" - Full FQDN: "acme.example.com"

Split on "." takes first component: - "acme.example.com" → ["acme", "example", "com"] → "acme" - "acme.example.com:8000" → "acme.example.com" → ["acme", ...] → "acme" - "example.com" → ["example", "com"] → "example"

Note: Port is NOT included in the domain part (handled by split):

# request.get_host() = "acme.example.com:8000"
host_without_port = "acme.example.com"
subdomain = "acme"

Examples of subdomain extraction
Host: "acme.example.com" → "acme"
Host: "globex.example.com" → "globex"
Host: "api.acme.example.com" → "api" (only first part)
Host: "example.com" → "example" (edge case)
Host: "acme.example.com:8000" → "acme" (port ignored)
Host: "my-tenant.example.com" → "my-tenant" (hyphens allowed)
Tenant Model Query

Queries the Tenant model:

Tenant = get_tenant_model()  # Get configured Tenant model
return Tenant.objects.get(tenant_id=subdomain)

The get_tenant_model() utility: - Returns configured Tenant model - Respects custom Tenant implementations - Loads from app registry - Default: BaseTenant

Query details: - Filters Tenant on tenant_id field - tenant_id is usually unique, indexed - Single query execution - Returns Tenant object

Error Handling

If no Tenant exists with matching tenant_id:

try:
    return Tenant.objects.get(tenant_id=subdomain)
except Tenant.DoesNotExist:
    raise TenantNotFound

Raises TenantNotFound when: - Subdomain doesn't match any tenant_id - Typo in subdomain - Tenant deleted but requests continue - Subdomain points to root domain

Not found cases: - "unknown.example.com" → no tenant_id="unknown" - "api.example.com" → no tenant_id="api" - "example.com" → may be no tenant_id="example"

Examples:

Successful resolution:
```python
# Tenant with tenant_id='acme' exists
request = RequestFactory().get('/')
request.META['HTTP_HOST'] = 'acme.example.com'

resolver = SubdomainTenantResolver()
tenant = resolver.resolve(request)
# Returns: Tenant(tenant_id='acme')
```

Different subdomain:
```python
request.META['HTTP_HOST'] = 'globex.example.com'
# Subdomain 'globex' extracted
# Tenant with tenant_id='globex' returned
```

With port number:
```python
request.META['HTTP_HOST'] = 'acme.example.com:8000'
# Subdomain 'acme' extracted (port not included)
# Tenant with tenant_id='acme' returned
```

Tenant not found:
```python
request.META['HTTP_HOST'] = 'unknown.example.com'
# Subdomain 'unknown' extracted
# No Tenant with tenant_id='unknown'
# Raises: TenantNotFound
```

Root domain:
```python
request.META['HTTP_HOST'] = 'example.com'
# Subdomain 'example' extracted (only first part)
# Usually no Tenant with tenant_id='example'
# Raises: TenantNotFound
# Solution: Configure default tenant or different resolver
```
Performance Characteristics
  • Single database query (SELECT * FROM tenant WHERE tenant_id=...)
  • No joins or relationships traversed
  • tenant_id field usually indexed
  • O(1) lookup time
  • Typically <1ms database time
  • Fastest resolver option
Comparison with CustomDomainTenantResolver

SubdomainTenantResolver: - Queries Tenant model directly - 1 database query per request - No custom domain table needed - Subdomain must equal tenant_id

CustomDomainTenantResolver: - Queries Domain model first, then gets tenant - 2 database queries per request - Requires Domain model with tenant FK - Multiple domains per tenant possible

Caching Strategy

For high-traffic applications, caching helps:

from django.core.cache import cache

cache_key = f'subdomain_tenant:{subdomain}'
tenant = cache.get(cache_key)
if tenant:
    return tenant

try:
    tenant = Tenant.objects.get(tenant_id=subdomain)
    # Cache for 1 hour
    cache.set(cache_key, tenant, timeout=3600)
    return tenant
except Tenant.DoesNotExist:
    raise TenantNotFound

Benefits: - Avoids database query on cache hit - Cache hit ratio typically 95%+ - Minimal overhead (cache lookup vs DB) - Suitable for high-concurrency

Testing
from django.test import TestCase
from django.test.client import RequestFactory

class TestSubdomainResolver(TestCase):
    def setUp(self):
        self.factory = RequestFactory()
        self.tenant = Tenant.objects.create(tenant_id='acme')

    def test_resolve_subdomain(self):
        request = self.factory.get('/')
        request.META['HTTP_HOST'] = 'acme.example.com'

        resolver = SubdomainTenantResolver()
        tenant = resolver.resolve(request)

        assert tenant == self.tenant

    def test_resolve_with_port(self):
        request = self.factory.get('/')
        request.META['HTTP_HOST'] = 'acme.example.com:8000'

        resolver = SubdomainTenantResolver()
        tenant = resolver.resolve(request)

        assert tenant == self.tenant

    def test_resolve_unknown_subdomain(self):
        request = self.factory.get('/')
        request.META['HTTP_HOST'] = 'unknown.example.com'

        resolver = SubdomainTenantResolver()

        with pytest.raises(TenantNotFound):
            resolver.resolve(request)

    def test_resolve_hyphenated_subdomain(self):
        tenant = Tenant.objects.create(tenant_id='acme-prod')
        request = self.factory.get('/')
        request.META['HTTP_HOST'] = 'acme-prod.example.com'

        resolver = SubdomainTenantResolver()
        resolved_tenant = resolver.resolve(request)

        assert resolved_tenant == tenant
See Also
  • base.py: Abstract resolver interface
  • customdomain_resolver.py: Custom domain-based alternative
  • models.py: BaseTenant model
  • middleware.py: Uses resolver for routing
  • exceptions.py: TenantNotFound exception
Source code in django_omnitenant/resolvers/subdomain_resolver.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
def resolve(self, request) -> object | None:
    """
    Resolve tenant from subdomain in request.

    Extracts the subdomain portion of the request host and uses it
    directly as the tenant_id to lookup the corresponding Tenant.

    Args:
        request (django.http.HttpRequest): The HTTP request
                                          Contains host/domain information

    Returns:
        BaseTenant: The tenant matching the subdomain

    Raises:
        TenantNotFound: If no tenant exists with tenant_id matching subdomain

    Process:
        1. Extract subdomain from request host
        2. Query Tenant model for matching tenant_id
        3. Return tenant if found
        4. Raise TenantNotFound if not found

    Subdomain Extraction:
        Subdomain is extracted by splitting host on dots:

        ```python
        subdomain = request.get_host().split(".")[0]
        ```

        request.get_host() returns the HTTP Host header:
        - Includes port if present: "acme.example.com:8000"
        - No port: "acme.example.com"
        - Full FQDN: "acme.example.com"

        Split on "." takes first component:
        - "acme.example.com" → ["acme", "example", "com"] → "acme"
        - "acme.example.com:8000" → "acme.example.com" → ["acme", ...] → "acme"
        - "example.com" → ["example", "com"] → "example"

        Note: Port is NOT included in the domain part (handled by split):
        ```python
        # request.get_host() = "acme.example.com:8000"
        host_without_port = "acme.example.com"
        subdomain = "acme"
        ```

    Examples of subdomain extraction:
        ```
        Host: "acme.example.com" → "acme"
        Host: "globex.example.com" → "globex"
        Host: "api.acme.example.com" → "api" (only first part)
        Host: "example.com" → "example" (edge case)
        Host: "acme.example.com:8000" → "acme" (port ignored)
        Host: "my-tenant.example.com" → "my-tenant" (hyphens allowed)
        ```

    Tenant Model Query:
        Queries the Tenant model:

        ```python
        Tenant = get_tenant_model()  # Get configured Tenant model
        return Tenant.objects.get(tenant_id=subdomain)
        ```

        The get_tenant_model() utility:
        - Returns configured Tenant model
        - Respects custom Tenant implementations
        - Loads from app registry
        - Default: BaseTenant

        Query details:
        - Filters Tenant on tenant_id field
        - tenant_id is usually unique, indexed
        - Single query execution
        - Returns Tenant object

    Error Handling:
        If no Tenant exists with matching tenant_id:

        ```python
        try:
            return Tenant.objects.get(tenant_id=subdomain)
        except Tenant.DoesNotExist:
            raise TenantNotFound
        ```

        Raises TenantNotFound when:
        - Subdomain doesn't match any tenant_id
        - Typo in subdomain
        - Tenant deleted but requests continue
        - Subdomain points to root domain

        Not found cases:
        - "unknown.example.com" → no tenant_id="unknown"
        - "api.example.com" → no tenant_id="api"
        - "example.com" → may be no tenant_id="example"

    Examples:

        Successful resolution:
        ```python
        # Tenant with tenant_id='acme' exists
        request = RequestFactory().get('/')
        request.META['HTTP_HOST'] = 'acme.example.com'

        resolver = SubdomainTenantResolver()
        tenant = resolver.resolve(request)
        # Returns: Tenant(tenant_id='acme')
        ```

        Different subdomain:
        ```python
        request.META['HTTP_HOST'] = 'globex.example.com'
        # Subdomain 'globex' extracted
        # Tenant with tenant_id='globex' returned
        ```

        With port number:
        ```python
        request.META['HTTP_HOST'] = 'acme.example.com:8000'
        # Subdomain 'acme' extracted (port not included)
        # Tenant with tenant_id='acme' returned
        ```

        Tenant not found:
        ```python
        request.META['HTTP_HOST'] = 'unknown.example.com'
        # Subdomain 'unknown' extracted
        # No Tenant with tenant_id='unknown'
        # Raises: TenantNotFound
        ```

        Root domain:
        ```python
        request.META['HTTP_HOST'] = 'example.com'
        # Subdomain 'example' extracted (only first part)
        # Usually no Tenant with tenant_id='example'
        # Raises: TenantNotFound
        # Solution: Configure default tenant or different resolver
        ```

    Performance Characteristics:
        - Single database query (SELECT * FROM tenant WHERE tenant_id=...)
        - No joins or relationships traversed
        - tenant_id field usually indexed
        - O(1) lookup time
        - Typically <1ms database time
        - Fastest resolver option

    Comparison with CustomDomainTenantResolver:
        SubdomainTenantResolver:
        - Queries Tenant model directly
        - 1 database query per request
        - No custom domain table needed
        - Subdomain must equal tenant_id

        CustomDomainTenantResolver:
        - Queries Domain model first, then gets tenant
        - 2 database queries per request
        - Requires Domain model with tenant FK
        - Multiple domains per tenant possible

    Caching Strategy:
        For high-traffic applications, caching helps:

        ```python
        from django.core.cache import cache

        cache_key = f'subdomain_tenant:{subdomain}'
        tenant = cache.get(cache_key)
        if tenant:
            return tenant

        try:
            tenant = Tenant.objects.get(tenant_id=subdomain)
            # Cache for 1 hour
            cache.set(cache_key, tenant, timeout=3600)
            return tenant
        except Tenant.DoesNotExist:
            raise TenantNotFound
        ```

        Benefits:
        - Avoids database query on cache hit
        - Cache hit ratio typically 95%+
        - Minimal overhead (cache lookup vs DB)
        - Suitable for high-concurrency

    Testing:
        ```python
        from django.test import TestCase
        from django.test.client import RequestFactory

        class TestSubdomainResolver(TestCase):
            def setUp(self):
                self.factory = RequestFactory()
                self.tenant = Tenant.objects.create(tenant_id='acme')

            def test_resolve_subdomain(self):
                request = self.factory.get('/')
                request.META['HTTP_HOST'] = 'acme.example.com'

                resolver = SubdomainTenantResolver()
                tenant = resolver.resolve(request)

                assert tenant == self.tenant

            def test_resolve_with_port(self):
                request = self.factory.get('/')
                request.META['HTTP_HOST'] = 'acme.example.com:8000'

                resolver = SubdomainTenantResolver()
                tenant = resolver.resolve(request)

                assert tenant == self.tenant

            def test_resolve_unknown_subdomain(self):
                request = self.factory.get('/')
                request.META['HTTP_HOST'] = 'unknown.example.com'

                resolver = SubdomainTenantResolver()

                with pytest.raises(TenantNotFound):
                    resolver.resolve(request)

            def test_resolve_hyphenated_subdomain(self):
                tenant = Tenant.objects.create(tenant_id='acme-prod')
                request = self.factory.get('/')
                request.META['HTTP_HOST'] = 'acme-prod.example.com'

                resolver = SubdomainTenantResolver()
                resolved_tenant = resolver.resolve(request)

                assert resolved_tenant == tenant
        ```

    See Also:
        - base.py: Abstract resolver interface
        - customdomain_resolver.py: Custom domain-based alternative
        - models.py: BaseTenant model
        - middleware.py: Uses resolver for routing
        - exceptions.py: TenantNotFound exception
    """
    # Extract subdomain from request host
    # request.get_host() returns HTTP Host header (e.g., "acme.example.com" or "acme.example.com:8000")
    # Split on "." to separate domain components
    # Take first element [0] which is the subdomain
    subdomain = request.get_host().split(".")[0]

    # Get the Tenant model (respects custom implementations via get_tenant_model utility)
    Tenant = get_tenant_model()

    try:
        # Query Tenant model for a tenant with matching tenant_id
        # Direct lookup - no joins or relationships
        # Returns Tenant object if found
        return Tenant.objects.get(tenant_id=subdomain)
    except Tenant.DoesNotExist:
        # No tenant exists with this tenant_id
        # Raise TenantNotFound exception (not None)
        # Middleware will catch and handle (typically 404)
        raise TenantNotFound