Pular para o conteúdo

Arquitetura de software · Estrutura do código · 8 min de leitura

Offline-first e sincronização

O app lê e grava num banco local e sincroniza com o servidor quando dá: funciona no metrô, abre na hora e não perde o que a pessoa fez — desde que os conflitos tenham regra.

Tela → Repositório: açãoaçãoRepositório → Banco local: grava jáBanco local → Tela: FlowFlowRepositório → OutboxOutbox → SincronizadorSincronizador → ServidorTela — observa o bancoTelaobserva o bancoRepositórioRepositórioBanco local — Room / SwiftDataBanco localRoom / SwiftDataOutbox — mudanças pendentesOutboxmudanças pendentesSincronizador — WorkManager / BGTaskSincronizadorWorkManager / BGTaskServidorServidor

Arraste para o lado para ver o diagrama inteiro.

A tela nunca espera a rede: grava no banco local, mostra na hora e a outbox envia quando houver conexão.

A ideia

Em vez de “buscar na API e mostrar”, a tela observa o banco local (Room com Flow no Android, SwiftData ou Core Data no iOS). Ler é instantâneo; gravar também — a mudança vai para o banco e para uma fila de pendências (a outbox). Um trabalho em segundo plano (WorkManager, BGTaskScheduler) envia as pendências e traz as novidades quando há rede, com retry e backoff.

Trazer novidades (pull)

  • Sincronização incremental: o servidor devolve só o que mudou depois de um cursor (updated_at ou número de versão), não a base inteira.
  • Exclusões: apagar de verdade no servidor faz o item “ressuscitar” em quem estava offline; use tombstones (marca de apagado) por um tempo.
  • IDs gerados no aparelho (UUID v7) permitem criar itens offline sem colisão.

Conflitos

Duas pessoas (ou dois aparelhos) alteraram o mesmo item offline. Estratégias, da mais simples à mais rica:

  • Última escrita vence (por horário do servidor): simples, mas descarta uma das edições.
  • Por campo: mescla quando cada um mudou campos diferentes.
  • Versão + rejeição: o servidor recusa a mudança baseada em versão velha e o app pede para a pessoa escolher.
  • CRDTs: estruturas que se mesclam sempre sem conflito (contadores, listas, texto colaborativo) — base do local-first.

Cuidados

  • Mostre o estado: “salvo no aparelho”, “sincronizado”, “erro ao enviar”.
  • Operações idempotentes no servidor (a outbox pode reenviar).
  • Migrações do banco local testadas — o dado do usuário mora ali.
  • Dados sensíveis no aparelho pedem criptografia e cuidado no logout.

No código

✓ ExemploKotlin
@Entity data class Note(@PrimaryKey val id: String, val text: String, val updatedAt: Long, val pending: Boolean)

class NotesRepository(private val dao: NoteDao, private val work: WorkManager) {
    /** A tela observa isto: abre na hora, com ou sem rede. */
    val notes: Flow<List<Note>> = dao.observeAll()

    suspend fun save(text: String, id: String = UUID.randomUUID().toString()) {
        dao.upsert(Note(id, text, System.currentTimeMillis(), pending = true))
        // Um único trabalho de sincronização na fila, que roda quando houver rede.
        work.enqueueUniqueWork(
            "sync-notes",
            ExistingWorkPolicy.KEEP,
            OneTimeWorkRequestBuilder<SyncNotesWorker>()
                .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
                .build(),
        )
    }
}

class SyncNotesWorker(ctx: Context, params: WorkerParameters, private val dao: NoteDao, private val api: NotesApi) :
    CoroutineWorker(ctx, params) {
    override suspend fun doWork(): Result = try {
        dao.pending().forEach { api.put(it); dao.markSynced(it.id, it.updatedAt) } // PUT por id: idempotente
        api.changesSince(dao.lastCursor()).let { dao.applyRemote(it) }
        Result.success()
    } catch (e: IOException) {
        Result.retry()
    }
}

Gravar marca o item como pendente e agenda um trabalho único com rede obrigatória e backoff; o worker envia as pendências com PUT por id (repetir não duplica) e depois puxa só o que mudou.

Em resumo

Vantagens e custos

Vantagens
  • App instantâneo e útil sem rede.
  • Nada se perde quando a conexão cai.
  • Menos carga no servidor com sincronização incremental.
Custos e riscos
  • Conflitos e migrações locais dão trabalho.
  • Dois modelos de dados (local e remoto) para manter.
  • Estado “pendente” precisa aparecer na interface.

Perguntar à IA

Monte um prompt pronto sobre Offline-first e sincronização na sua linguagem e no seu caso, e abra num chat gratuito.

ChatGPT Claude Perplexity

O prompt vai para o site que você escolher. Não cole senhas, tokens nem dados reais de clientes. Para Gemini e outros, copie e cole.

Referências

Continue aprendendo

Todos os temas de arquitetura de software →