michangarrito/docs/97-adr/ADR-0006-storage-abstraction.md
rckrdmrd 2c916e75e5 [SIMCO-V4] feat: Agregar documentación SaaS, ADRs e integraciones
Nuevas Épicas (MCH-029 a MCH-033):
- Infraestructura SaaS multi-tenant
- Auth Social (OAuth2)
- Auditoría Empresarial
- Feature Flags
- Onboarding Wizard

Nuevas Integraciones (INT-010 a INT-014):
- Email Providers (SendGrid, Mailgun, SES)
- Storage Cloud (S3, GCS, Azure)
- OAuth Social
- Redis Cache
- Webhooks Outbound

Nuevos ADRs (0004 a 0011):
- Notifications Realtime
- Feature Flags Strategy
- Storage Abstraction
- Webhook Retry Strategy
- Audit Log Retention
- Rate Limiting
- OAuth Social Implementation
- Email Multi-provider

Actualizados:
- MASTER_INVENTORY.yml
- CONTEXT-MAP.yml
- HERENCIA-SIMCO.md
- Mapas de documentación

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 01:43:15 -06:00

4.5 KiB

id type title status decision_date updated_at simco_version stakeholders tags
ADR-0006 ADR Storage Abstraction Accepted 2026-01-10 2026-01-10 4.0.1
Equipo MiChangarrito
storage
s3
r2
abstraction
multi-cloud

ADR-0006: Storage Abstraction

Metadata

Campo Valor
ID ADR-0006
Estado Accepted
Fecha 2026-01-10
Autor Architecture Team
Supersede -

Contexto

MiChangarrito necesita almacenar archivos (imagenes de productos, facturas, documentos) en la nube. Queremos:

  1. Flexibilidad para cambiar de proveedor
  2. Desarrollo local sin depender de cloud
  3. Optimizacion de costos (R2 es mas barato que S3)

Decision

Implementamos una capa de abstraccion con Factory Pattern que soporta S3, Cloudflare R2 y MinIO.

Todos los proveedores implementan la misma interfaz, permitiendo cambiar de proveedor sin modificar codigo de negocio.

interface StorageProvider {
  upload(key: string, file: Buffer, options?: UploadOptions): Promise<UploadResult>;
  download(key: string): Promise<Buffer>;
  delete(key: string): Promise<void>;
  getSignedUrl(key: string, expiresIn: number): Promise<string>;
  list(prefix: string): Promise<FileInfo[]>;
}

Alternativas Consideradas

Opcion 1: Usar S3 directamente

  • Pros:
    • Simple
    • Bien documentado
  • Cons:
    • Vendor lock-in
    • Sin desarrollo local facil
    • Costos pueden ser altos

Opcion 2: Abstraccion con Factory (Elegida)

  • Pros:
    • Flexibilidad de proveedor
    • MinIO para desarrollo local
    • Optimizacion de costos con R2
    • Codigo limpio
  • Cons:
    • Complejidad adicional
    • Mantener multiples providers

Opcion 3: Usar libreria existente (flydrive)

  • Pros:
    • Ya implementado
    • Multiples drivers
  • Cons:
    • Dependencia externa
    • Menos control
    • Puede no cubrir todos nuestros casos

Consecuencias

Positivas

  1. Flexibilidad: Cambiar proveedor sin impacto en negocio
  2. Desarrollo: MinIO local sin credenciales cloud
  3. Costos: Migrar a R2 reduce costos ~75%
  4. Testing: Facil mock de storage

Negativas

  1. Complejidad: Mantener 3 implementaciones
  2. Features especificos: Podemos perder algunas features unicas

Implementacion

Factory

@Injectable()
export class StorageFactory {
  create(provider: StorageProviderType): StorageProvider {
    switch (provider) {
      case 's3':
        return new S3StorageProvider(this.configService);
      case 'r2':
        return new R2StorageProvider(this.configService);
      case 'minio':
        return new MinIOStorageProvider(this.configService);
      default:
        throw new Error(`Unknown storage provider: ${provider}`);
    }
  }
}

Provider S3

class S3StorageProvider implements StorageProvider {
  private client: S3Client;

  constructor(config: ConfigService) {
    this.client = new S3Client({
      region: config.get('S3_REGION'),
      credentials: {
        accessKeyId: config.get('S3_ACCESS_KEY'),
        secretAccessKey: config.get('S3_SECRET_KEY'),
      },
    });
  }

  async upload(key: string, file: Buffer, options?: UploadOptions): Promise<UploadResult> {
    const command = new PutObjectCommand({
      Bucket: this.bucket,
      Key: key,
      Body: file,
      ContentType: options?.contentType,
    });

    await this.client.send(command);

    return {
      key,
      url: `https://${this.bucket}.s3.amazonaws.com/${key}`,
    };
  }

  // ... otros metodos
}

Uso

@Injectable()
export class FileService {
  constructor(private readonly storageFactory: StorageFactory) {}

  async uploadProductImage(productId: string, file: Buffer): Promise<string> {
    const storage = this.storageFactory.create(process.env.STORAGE_PROVIDER);
    const key = `products/${productId}/image.jpg`;

    const result = await storage.upload(key, file, {
      contentType: 'image/jpeg',
    });

    return result.url;
  }
}

Configuracion por Ambiente

Ambiente Proveedor Razon
Development MinIO Local, sin credenciales
Staging R2 Costos bajos
Production S3 o R2 Segun necesidad

Referencias


Fecha decision: 2026-01-10 Autores: Architecture Team