Executando verificação de segurança...
1

Meus 2 cents,

Entendi a ideia, mas o buraco eh um pouco mais embaixo.

Quando falamos de "rollback" precisa considerar todo o contexto de entidades interligadas - voltar apenas 1 entidade e presumir que "esta tudo bem" nao eh uma boa estrategia.

Imagine que o cliente fez a besteira, outras acoes foram efetuadas (como pedidos, nf, reembolsos, etc) e voce faz o "rollback" apenas de 1 entidade (p.ex. cliente) e isso causa um descompasso nas demais - acaba virando um pesadelo para entender o que esta correto e o que nao esta.

Ja vi esse cenario via "updates" com "wheres" mal feitos.

Nao existe saida facil aqui - trazer o backup mais recente ou ultimo snapshot do banco quase sempre implica em perder transacoes que precisam ser reconciliadas manualmente, e isso da um trabalho insano.

Uma possibilidade de atualizacao na tua proposta seria o mapeamento de dados e entidades que teriam reflexo na atualizacao e fazer snapshot delas tambem, e assim por diante - mas nao vejo isso como uma saida pratica/viavel.

Carregando publicação patrocinada...
1

Depois da nossa conversa inicial, decidi atualizar o exemplo pra ficar mais fácil de visualizar a ideia enquanto um mecanismo de compensação a nível de negócio, ao invés de estar associado à camada de dados. Isso porque, na camada de dados, a reconciliação vai certamente cair nas complicações que você muito bem colocou. Já na camada de negócios, seria o mesmo que delegar uma atualização em alto nível.
Não resolve todos os casos, mas pode ser o suficiente para alguns cenários.

graph TB
    %% Client Layer
    subgraph CLIENT ["🌐 CLIENT LAYER"]
        HTTP["HTTP Requests<br/>GET /health<br/>GET /users<br/>POST /users<br/>PUT /users/:email<br/>POST /users/:email/rollback"]
    end

    %% Presentation Layer
    subgraph PRESENTATION ["📡 PRESENTATION LAYER"]
        APP["Express App<br/>(index.ts)<br/>+ Pino Logger"]
        ROUTES["Routes Handler<br/>(routes.ts)"]
    end

    %% Use Case Layer (Refactored)
    subgraph USECASE ["🎯 USE CASE LAYER"]
        USERSUSECASES["UsersUseCases Class<br/>• createUser()<br/>• updateUser()<br/>• listUsers()<br/>• rollbackUser()"]
        SNAPSUSECASES["SnapshotsUseCases Class<br/>• createSnapshot()<br/>• getSnapshot()"]
        USECASEINSTANCE["Singleton Instance<br/>(use-case/users/index.ts)"]
    end

    %% Repository Layer
    subgraph REPOSITORY ["🗄️ REPOSITORY LAYER"]
        USERREPOINTF["UsersRepository<br/>Interface"]
        USERREPO["UsersRepositoryInMemory<br/>(Implementation)"]
        SNAPREPOINTF["SnapshotRepository<br/>Interface"]
        SNAPREPO["SnapshotRepositoryInMemoryImpl<br/>(Implementation)"]
    end

    %% Entity Layer
    subgraph ENTITY ["📊 ENTITY LAYER"]
        USER["User Interface<br/>{name: string, email: string}"]
        SNAPSHOT["Snapshot Interface<br/>{entity_id: string, entity: string, data: string}"]
    end

    %% Data Storage
    subgraph STORAGE ["💾 DATA STORAGE"]
        USERMAP["In-Memory Map<string, User><br/>User Storage<br/>+ Duplicate Prevention"]
        SNAPMAP["In-Memory Map<string, Snapshot><br/>Snapshot Storage<br/>Key: 'entity@entity_id'"]
    end

    %% Communication Flow
    HTTP --> APP
    APP --> ROUTES
    
    ROUTES --> USECASEINSTANCE
    USECASEINSTANCE --> USERSUSECASES
    
    USERSUSECASES --> USERREPOINTF
    USERREPOINTF --> USERREPO
    
    USERSUSECASES --> SNAPSUSECASES
    SNAPSUSECASES --> SNAPREPOINTF
    SNAPREPOINTF --> SNAPREPO

    USERREPO --> USER
    SNAPREPO --> SNAPSHOT

    USERREPO --> USERMAP
    SNAPREPO --> SNAPMAP

    %% Dependency Injection
    USECASEINSTANCE -.->|"Dependency Injection"| USERREPO
    USERSUSECASES -.->|"Composition"| SNAPSUSECASES
    SNAPSUSECASES -.->|"Dependency Injection"| SNAPREPO

    %% Enhanced Features
    USERREPO -.->|"Duplicate Check"| USERMAP
    SNAPREPO -.->|"Composite Key Storage"| SNAPMAP

    %% Styling for high contrast
    classDef clientStyle fill:#000000,stroke:#ffffff,stroke-width:3px,color:#ffffff
    classDef presentationStyle fill:#2d3748,stroke:#ffffff,stroke-width:2px,color:#ffffff
    classDef usecaseStyle fill:#4a5568,stroke:#ffffff,stroke-width:2px,color:#ffffff
    classDef repositoryStyle fill:#718096,stroke:#000000,stroke-width:2px,color:#000000
    classDef entityStyle fill:#e2e8f0,stroke:#000000,stroke-width:2px,color:#000000
    classDef storageStyle fill:#ffffff,stroke:#000000,stroke-width:3px,color:#000000

    class HTTP clientStyle
    class APP,ROUTES presentationStyle
    class USERSUSECASES,SNAPSUSECASES,USECASEINSTANCE usecaseStyle
    class USERREPOINTF,USERREPO,SNAPREPOINTF,SNAPREPO repositoryStyle
    class USER,SNAPSHOT entityStyle
    class USERMAP,SNAPMAP storageStyle

No código ficou assim:

class UsersUseCases {
  private snapshotsUseCases: SnapshotsUseCases;
  private _ENTITY = 'user';

  constructor(private readonly usersRepository: UsersRepository) {
    this.snapshotsUseCases = new SnapshotsUseCases(new SnapshotRepositoryInMemoryImpl());
  }

  createUser(user: User) {
    this.usersRepository.store(user);
  }

  updateUser(user: User) {
    const currentState = this.usersRepository.findByEmail(user.email);

    if (currentState) {
      this.snapshotsUseCases.createSnapshot({
        entity: this._ENTITY,
        entity_id: user.email,
        data: JSON.stringify(currentState)
      });
    }

    this.usersRepository.update(user);
  }

  listUsers() {
    return this.usersRepository.list();
  }

  rollbackUser(email: string) {
    const previousState = this.snapshotsUseCases.getSnapshot(this._ENTITY, email);
    if (previousState) {
      const user = JSON.parse(previousState.data) as User;
      this.updateUser(user);
    }
  }
}
1

Você trouxe bons pontos. E dada a proposta simplificada que usei pra apresentar a ideia, realmente, fica inviável aplicar em cenários reais.
Acredito que falhei em não adicionar a este contexto qual foi a inspiração, e olhando agora, acho que dava pra fazer uma implementação mais completa sem perder a simplicidade.

Mas a ideia da abstração foi "chupinhada" da abordagem de compensação de transações que o "Saga Pattern" implementa para ambientes distribuídos, aonde cada etapa da transação possui um mecanismo de compensação para que, em eventual falha na cadeia de um workflow distribuído, seja possível a reversão da transação.

Como ali, injetei o snapshot na camada de gestão de dados fica ainda mais complicado de associar com a ideia que mencionei acima.

Mas usando este comentário como oportunidade pra acrescentar um pouco mais ao texto, Eu diria que o ideal é que em camadas superiores, um workflow possa permitir tirar uma "fotografia" do estado atual das entidades relacionadas à operação, e ao gravar isso, possamos retroceder se necessário.
Isso faria da compensação, em linhas bem gerais, uma operação de atualização que reutiliza os dados anteriores à última edição.

Por exemplo, no caso do código que coloquei de exemplo, o correto seria mover a gestão do snapshot para a camada de negócios (casos de uso).

Bons estudos e um forte abraço!

1

Meus 2 cents extendidos,

Refletindo mais um pouco - tem outro aspecto que tua publicacao traz a tona.

Antigamente, o usuario final tinha poucas possibilidades de alterar em lote grandes quantidades de dados de modo nao-supervisionado: para fazer este tipo de alteracao ou era atraves de alguma opcao do sistema (que tinha algum tipo de salvaguarda, como p.ex. permitir alteracoes em campos nao criticos) ou entao pedir para o TI fazer a alteracao (garantindo assim um minimo de analise da tarefa).

Quando a IA comeca a ter acesso a endpoints diretamente, e por consequencia, permitir ao usuario criar suas automacoes que podem afetar dezenas/milhares de dados em uma unica acao - talvez tenhamos de repensar metodologias de ACID/commit/transaction que tambem consideram este tipo de cenario.

Como foi comentado - snapshots poderiam ser um caminho, mas manter a integridade das regras de negocio em situacoes de rollback eh algo bem complicado: afinal, se um cliente recebeu uma notificacao de pedido aceito e faturamento realizado - como lidar com um rollback em algumas das entidades envolvidas ?

1

Perfeito. Bem colocado.
Para cenários aonde o impacto do rollback não envolve apenas entidades internas, de fato, não há uma resposta simples e talvez, a abordagem de compensação seja apenas a ponta de um iceberg, que certamente, permite muita exploração para melhorias futuras.
Acredito que esse tipo de padrão de compensação de transações vai se tornar cada vez mais comum, inclusive, a implantação de um 2 phase commit quando lidarmos com operações críticas.
Acredito muito que a discussão desse tipo de garantia de "consistência intencional" - com "intencional", estou tentando traduzir a ideia de que a consistência precisa estar atrelada à intenção de alterar o estado de uma entidade e mantê-la, na falta de uma expressão mais precisa - será cada vez mais frequente.
E falo isso com a propriedade de alguém que tem participado ativamente deste tipo de conversa, e enfrentado problemas desta natureza no dia-a-dia.

Um forte abraço!