Lean Caching – A Custom NetworkBoundResource in Kotlin

Orchestrating data between a remote API and a local database is a foundational problem in mobile development. While the industry consensus rightly leans toward the Single Source of Truth pattern, the implementation often degrades into massive, fragile repository classes littered with boilerplate try-catch blocks and manual thread switching.

Solving this does not require a heavy, third-party state-management library. A clean, generic networkBoundResource built with Kotlin Flows and Coroutines is highly effective, provided you leverage Kotlin’s advanced language features correctly.

Let me explain by demonstrating this pattern in my RandomPokemon project. By focusing on reified generics, inline functions, and structured concurrency, I’ve created a highly fault-tolerant data layer that I drop into my projects to handle caching without the bloat.

State Representation: Rethinking ResultState

Orchestrating complex data flows requires a strict contract for state representation. A standard sealed class handles the basic loading, success, and error states, but the mechanism used to unwrap that state dictates the cleanliness of the presentation layer. The versatility of the ResultState lends itself well to be used in API call returns, I/O operations, or for use cases being observed by the ViewModel.

sealed class ResultState {
    data class Complete<out O>(val data: O?) : ResultState()
    data class Running(val message: String? = null) : ResultState()
    data class Error(val error: Throwable? = null) : ResultState()

    inline fun <reified O> parse(
        onRunning: (String?) -> Unit = {},
        onError: (Throwable?) -> Unit = {},
        onComplete: (O?) -> Unit = {},
    ) {
        when (this) {
            is Running -> onRunning(this.message)
            is Error -> onError(this.error)
            is Complete<*> -> this.data?.let {
                it.takeIf { it is O }?.let { result -> onComplete(result as O?) }
                    ?: run { onError(IllegalArgumentException("Expected output of type ${O::class.simpleName}; got ${data?.let { data::class.simpleName }}")) }
            } ?: run { onComplete(null) }
        }
    }
}

Standard state implementations often force the caller to write repetitive when statements and perform unsafe casts in the UI or ViewModel. I intentionally avoid this by relying on the inline fun parse function to safely handle type checking and casting at runtime. If the type does not match, it gracefully routes to the onError block, preventing an abrupt ClassCastException and keeping the app’s crash-free rate intact.

The Orchestrator: networkBoundResource

My core orchestrator is a single, highly optimized function. Relying heavily on inline and crossinline, it eliminates the runtime overhead associated with the higher-order functions passed into it.

inline fun <reified DomainType, reified DtoType> networkBoundResource(
    crossinline query: () -> Flow<ResultState>,
    crossinline fetch: suspend () -> ResultState,
    crossinline saveFetchResult: suspend (DtoType) -> Unit,
    crossinline shouldFetch: (DomainType?) -> Boolean = { it == null },
    crossinline mapDtoToDomain: (DtoType) -> DomainType,
    crossinline onCachePresent: () -> Unit = {}
) = flow {
    emit(ResultState.Running("Checking cache..."))
    val cachedDataResult = query().last()

    var cachedData: DomainType? = null
    cachedDataResult.parse<DomainType>(
        onError = { Timber.w(it, "Cache returned an error.") }
    ) { cachedData = it }

    if (shouldFetch(cachedData)) {
        emit(ResultState.Running("Cache miss. Fetching from remote..."))

        when (val remoteResult = fetch()) {
            is ResultState.Complete<*> -> {
                val dto = remoteResult.data as? DtoType

                if (dto != null) {
                    emit(ResultState.Complete(mapDtoToDomain(dto)))

                    supervisorScope {
                        launch {
                            try {
                                saveFetchResult(dto)
                            } catch (e: Exception) {
                                Timber.e(e, "Failed to save network resource to cache.")
                            }
                        }
                    }
                } else {
                    emit(ResultState.Error(IllegalStateException("Type mismatch: Remote fetch did not return the expected DTO.")))
                }
            }
            is ResultState.Error -> emit(remoteResult)
            is ResultState.Running -> emit(remoteResult)
        }
    } else {
        onCachePresent()
        emit(ResultState.Complete(cachedData!!))
    }
}

Two specific architectural decisions drive this implementation, focusing on performance and fault tolerance:

  1. Immediate UI Emissions: When the remote fetch succeeds, the code emits the Complete state before initiating the database save. Disk I/O is slow. Forcing the UI to wait for Room to finish inserting records is an unnecessary bottleneck. The user gets the mapped domain model instantly.

  2. Detached Caching: Launching saveFetchResult inside a supervisorScope acts as a defensive shield. If the database insertion fails due to a constraint violation or disk issue, the exception is caught and logged by Timber. The overarching coroutine running the Flow does not crash, and the UI retains the successfully fetched in-memory data.

Implementation: Concurrent Caching

An abstraction is only as effective as its call site. Integrating this into my PokemonRepoImpl demonstrates how I handle complex, relational data saving without blocking the thread.

class PokemonRepoImpl(
    private val remote: PokemonRepo.Remote,
    private val local: PokemonRepo.Local,
    private val dispatchers: DispatcherProvider
) : PokemonRepo {
    override fun getPokemonById(id: Long) = networkBoundResource<PokemonDetails, PokemonDto>(
        query = { local.getPokemonById(id) },
        fetch = { remote.getPokemonById(id).last() },
        saveFetchResult = { dto -> cacheRemotePokemon(dto.toDomainModel()) },
        mapDtoToDomain = { dto -> dto.toDomainModel() }
    )

    private suspend fun cacheRemotePokemon(details: PokemonDetails) {
        supervisorScope {
            val saveOperations: List<Pair<String, suspend () -> Unit>> = listOf(
                "pokemon" to { local.savePokemon(details.pokemon) },
                "cries" to { local.saveCries(details.cries) },
                "sprites" to { local.saveSprites(details.sprites) },
                "abilities" to { local.saveAbilities(details.abilities) }
            )

            saveOperations.forEach { (name, action) ->
                launch(dispatchers.io) {
                    try {
                        action()
                    } catch (e: Exception) {
                        Timber.e(e, "Error caching $name")
                    }
                }
            }
        }
    }
}

Mapping a large API payload to a relational database requires splitting the data into multiple tables. Executing these inserts sequentially scales poorly.

By wrapping the inserts in a supervisorScope and iterating through a list of saveOperations with launch(dispatchers.io), this repository performs concurrent database inserts. More importantly, the supervisorScope ensures fault isolation. If the API returns a malformed URL for a sprite that violates a database constraint, that specific coroutine fails and logs an error, but the parent scope remains active, allowing the pokemon, cries, and abilities to be cached successfully.

Final Thoughts

This implementation is my take on a pragmatic approach to Android architecture. Combining reified generics for type safety, inline functions for zero-overhead abstractions, and structured concurrency for fault-tolerant caching – yields a repository layer that is highly readable and extremely resilient. It solves a universal problem by leaning on the language, entirely avoiding the need for bloated external dependencies.

Stop Using the APIC GUI: Automate Cisco ACI with Terraform and Nexus-as-Code

If your data center team is still provisioning ACI tenants through point-and-click in 2026, the tooling isn’t the problem — the tooling has been mature for years. Terraform’s ACI provider shipped in 2019. Cisco’s Nexus-as-Code removed the HCL learning curve in 2022. Brownfield import means zero excuses for existing fabrics.

This guide walks through the full path: raw Terraform HCL → Nexus-as-Code YAML → CI/CD pipeline with peer-reviewed network changes. Whether you’re managing 5 tenants or 500, the workflow is the same.

Why Terraform for ACI?

Manual APIC GUI provisioning takes 15–30 minutes per tenant with VRF, bridge domain, and EPG creation. A terraform apply does the same in under 60 seconds. But speed is the least interesting benefit — the real value is drift detection, peer review, and rollback capability.

The critical distinction vs. a Python script: idempotency. A script that creates a tenant will fail or create a duplicate if you run it twice. Terraform checks current state first — if the tenant exists and matches your code, it does nothing. If someone manually changed the VRF through the GUI, terraform plan shows exactly what drifted.

Approach Re-run Behavior Drift Detection Rollback Best For
Python + APIC REST Fails or duplicates None Manual backup Quick prototyping
Ansible ACI Modules Idempotent per task Limited Re-run playbook Config pushes
Terraform ACI Provider Idempotent by design Built-in state comparison apply with previous code Full lifecycle mgmt
Nexus-as-Code (NAC) Idempotent + YAML Built-in + schema validation git revert + apply Enterprise scale

Raw Terraform: Your First ACI Tenant

The CiscoDevNet/aci provider (v2.x) supports 90+ resources. Every HCL resource maps 1:1 to an ACI managed object — aci_tenant → fvTenant, aci_vrf → fvCtx, aci_bridge_domain → fvBD.

# providers.tf
terraform {
  required_providers {
    aci = {
      source  = "CiscoDevNet/aci"
      version = "~> 2.15"
    }
  }
}

provider "aci" {
  username = var.apic_username
  password = var.apic_password
  url      = var.apic_url
  insecure = false
}

# main.tf
resource "aci_tenant" "prod" {
  name        = "PROD-Web"
  description = "Production web services tenant"
}

resource "aci_vrf" "prod_vrf" {
  tenant_dn              = aci_tenant.prod.id
  name                   = "PROD-VRF"
  ip_data_plane_learning = "enabled"
}

resource "aci_bridge_domain" "web_bd" {
  tenant_dn          = aci_tenant.prod.id
  name               = "WEB-BD"
  relation_fv_rs_ctx = aci_vrf.prod_vrf.id
  arp_flood          = "no"
  unicast_route      = "yes"
}

terraform init → plan → apply. Entire operation: under 10 seconds against a lab APIC.

Auth: Don’t Hardcode Credentials

export ACI_USERNAME=admin
export ACI_PASSWORD=$(vault kv get -field=password secret/apic)
export ACI_URL=https://apic1.lab.local

The provider reads ACI_USERNAME, ACI_PASSWORD, and ACI_URL automatically. For production, integrate with HashiCorp Vault or your org’s secrets manager.

Nexus-as-Code: YAML Instead of HCL

Nexus-as-Code (NAC) is a Cisco-maintained Terraform module with 150+ sub-modules that translates plain YAML into Terraform ACI resources. Instead of writing individual HCL blocks for every object, you define your fabric in YAML and NAC handles the translation.

The same tenant from above in NAC YAML:

# data/tenants.yaml
apic:
  tenants:
    - name: PROD-Web
      description: "Production web services tenant"
      vrfs:
        - name: PROD-VRF
          ip_data_plane_learning: enabled
      bridge_domains:
        - name: WEB-BD
          vrf: PROD-VRF
          arp_flood: false
          unicast_route: true
          subnets:
            - ip: 10.1.100.1/24
              public: true
              shared: false
      application_profiles:
        - name: Web-App
          endpoint_groups:
            - name: Web-EPG
              bridge_domain: WEB-BD
              physical_domains:
                - PHY-DOM

And the entire main.tf:

module "aci" {
  source  = "netascode/nac-aci/aci"
  version = "0.9.3"

  yaml_directories = ["data"]
}

That’s it. NAC parses the YAML, creates the resources, handles dependency ordering, and manages relationship bindings. For a network engineer who knows ACI but isn’t a developer, this is the difference between a 2-week HCL learning curve and a 2-hour one.

How NAC Works Under the Hood

  1. Your YAML files define desired ACI state in data/
  2. main.tf loads YAML via yaml_directories
  3. NAC root module parses and routes objects to sub-modules
  4. Sub-modules contain aci_rest_managed resources → APIC REST API calls
  5. Terraform state records what was deployed for drift detection

You never touch raw API calls — only YAML files.

Brownfield Import: The Step Everyone Skips

This is the single most critical step for existing ACI deployments — and the one most commonly skipped.

Without importing existing objects, terraform apply either creates duplicates (APIC rejects with 400) or creates conflicting configs. Both outcomes are bad.

Bulk Import with nac-import

# Clone and run
nac-import --url https://apic1.lab.local --username admin

# Review generated YAML
ls data/

# Verify — plan should show zero changes
terraform init && terraform plan

# Commit — your fabric is now under version control
git add . && git commit -m "Import existing ACI fabric"

Selective Import

If you only want specific tenants under Terraform control:

terraform import aci_tenant.prod uni/tn-PROD-Web
terraform import aci_vrf.prod_vrf uni/tn-PROD-Web/ctx-PROD-VRF
terraform plan  # Should show no changes for imported resources

Remote State: Don’t Keep It on Your Laptop

The state file is Terraform’s record of every managed object. Without it, Terraform can’t detect drift or plan changes. State locking prevents two engineers from running apply simultaneously.

Backend Locking Best For Cost
HCP Terraform Built-in Managed runs + approval UI Free tier
S3 + DynamoDB DynamoDB lock AWS environments ~$1/mo
Azure Blob Blob lease Azure environments ~$1/mo
GitLab Managed Built-in GitLab CI/CD teams Included
terraform {
  backend "s3" {
    bucket         = "aci-terraform-state"
    key            = "prod/aci/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Separate state per environment and per fabric. A destroy on dev can’t touch production.

CI/CD Pipeline: Peer-Reviewed Network Changes

The ultimate win — treating network changes like software deployments:

# .github/workflows/aci-deploy.yml
name: ACI Terraform Pipeline
on:
  pull_request:
    paths: ['aci/**']
  push:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init -backend=false
      - run: terraform validate
      - run: terraform fmt -check -recursive

  plan:
    needs: validate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan -out=tfplan -no-color

  apply:
    needs: plan
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production  # Manual approval gate
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform apply -auto-approve

The environment: production gate requires a designated approver to click “Approve” in GitHub before apply runs. Automated, auditable, enforced.

Common Pitfalls

Pitfall Consequence Fix
Skip brownfield import 400 errors or duplicate objects Always nac-import or terraform import first
Local state file Lost laptop = lost state Remote backend with locking on day one
Single state for all envs Dev mistake destroys prod Separate state per env/fabric
Hardcoded credentials Secrets in Git Environment variables + Vault
No plan review Unreviewed changes hit production CI/CD with mandatory approval gate
Monolithic main.tf 2000-line files nobody can review Split: tenants.tf, access.tf, fabric.tf

Pin Your Provider Version

required_providers {
  aci = {
    source  = "CiscoDevNet/aci"
    version = "~> 2.15"
  }
}

The ~> operator allows patch updates (2.15.x) but blocks minor bumps (2.16.0). Provider updates have historically changed attribute names — a floating version can break your plan unexpectedly.

FAQ

Do I need programming experience?
No. HCL is declarative, not a programming language. With NAC, you only write YAML. Git fundamentals matter more than coding.

Terraform vs. Ansible for ACI?
Terraform manages full lifecycle declaratively (create, update, destroy with state tracking). Ansible is imperative and task-oriented. Common pattern: Terraform for day-0/day-1 provisioning, Ansible for day-2 operations.

How do I handle existing ACI configs?
Use nac-import for bulk import or terraform import for selective management. Never write HCL for objects that already exist without importing them first.

Originally published at FirstPassLab. For more deep-dive networking guides, check out firstpasslab.com.

AI Disclosure: This article was adapted from human-researched content with AI assistance for formatting and Dev.to optimization.

YÖK Başkanı Özvar: Tarım ve hayvancılık milli güvenlik meselesi haline geldi

Yükseköğretim Kurulu (YÖK) Başkanı Prof. Dr. Erol Özvar, tarım ve hayvancılığın ekonomik, milli güvenlik ve toplumsal refah açısından kritik bir öneme sahip olduğunu belirterek YÖK ve Tarım Teknolojileri Kümelenmesi Vakfı (TÜME) arasında iş birliği protokolü imzalandığını duyurdu. Protokol ile 40 bin gence istihdam imkanı sağlanması hedefleniyor.

Kripto Varlık Düzenlemeleri Geri Çekildi

AK Parti Genel Başkan Yardımcısı Ömer İleri, TBMM Genel Kurulunda görüşmeleri devam eden ekonomiye ilişkin düzenlemeleri de içeren Bazı Kanunlarda Değişiklik Yapılmasına Dair Kanun Teklifi’nde yer alan kripto varlıklara ilişkin maddelerin, sektördeki hızlı değişim ve gelişmeler göz…

MKE, havacılık ve savunma kalite mimarisinin merkezinde yer alan IAQG Europe'a kabul edildi

Makine ve Kimya Endüstrisi (MKE), havacılık, uzay ve savunma sanayiinde uluslararası kalite standartlarının oluşturulmasına yön veren IAQQ Europe Associate Member statüsüne kabul edildi. Bu üyelik, MKE’nin küresel kalite ekosistemindeki etkinliğini artırarak kalite yönetimi ve tedarik zinciri entegrasyonu alanında önemli katkılar sağlayacak.

Komisyondan 'Türkiye Yapay Zeka Kurumu' önerisi

TBMM Yapay Zeka Araştırma Komisyonu raporunda, ‘Yapay Zeka Diplomasi Stratejisi’, ‘Avrupa Konseyi Yapay Zeka Çerçeve Sözleşmesi’nin imzalanması’, ‘Ulusal Yapay Zeka Güvenlik Stratejisinin hazırlanması’, ‘TBMM nezdinde bir, ‘Yapay Zeka, İleri Teknolojiler ve Yenilik Komisyonu’ kurulması’,…