Diretrizes de Desenvolvimento
2. Arquitetura de Software

2. Arquitetura de Software

2.1. Visão geral das arquiteturas utilizadas

Nossa empresa emprega uma variedade de arquiteturas de software, cada uma escolhida estrategicamente para atender às necessidades específicas de cada produto. As principais arquiteturas que utilizamos são:

  1. Monolito: Utilizada principalmente em nosso produto principal, o Toolzz LMS.
  2. Microserviços: Implementada em produtos como Toolzz AI e Toolzz Chat.
  3. Serverless: Aplicada em componentes específicos do Toolzz Connect.
  4. Event-Driven: Utilizada em partes do Toolzz Bots para processamento assíncrono.

A escolha da arquitetura para cada produto é baseada em fatores como escalabilidade necessária, complexidade do domínio, requisitos de performance e velocidade de desenvolvimento.

2.2. Monolito Laravel (Toolzz LMS)

O Toolzz LMS, nosso produto principal, é construído como um monolito Laravel. Esta escolha arquitetural oferece várias vantagens para este produto específico:

Características principais:

  • Estrutura unificada: Todo o código está em um único repositório, facilitando o desenvolvimento e deploy.
  • Shared-nothing architecture: Cada instância do aplicativo pode funcionar completamente independente.
  • Modularização interna: Utilizamos o conceito de módulos do Laravel para organizar funcionalidades.

Boas práticas para o monolito Laravel:

  1. Organização de código:

    • Use namespaces para organizar o código (ex: App\Modules\Courses, App\Modules\Users).
    • Mantenha controllers enxutos, movendo lógica de negócios para Services.
    namespace App\Modules\Courses\Controllers;
     
    use App\Modules\Courses\Services\CourseService;
     
    class CourseController extends Controller
    {
        private $courseService;
     
        public function __construct(CourseService $courseService)
        {
            $this->courseService = $courseService;
        }
     
        public function store(CourseRequest $request)
        {
            $course = $this->courseService->createCourse($request->validated());
            return response()->json($course, 201);
        }
    }
  2. Camada de serviço:

    • Implemente uma camada de serviço para encapsular a lógica de negócios.
    • Use injeção de dependência para facilitar testes e manter o código desacoplado.
  3. Padrão Repository:

    • Use o padrão Repository para abstrair a camada de persistência.
    • Isso facilita a mudança de ORM ou até mesmo de banco de dados no futuro.
    namespace App\Modules\Courses\Repositories;
     
    use App\Modules\Courses\Models\Course;
     
    class CourseRepository
    {
        public function create(array $data): Course
        {
            return Course::create($data);
        }
     
        public function findById(int $id): ?Course
        {
            return Course::find($id);
        }
    }
  4. Caching:

    • Implemente caching em níveis apropriados para melhorar a performance.
    • Use Redis para caching distribuído.
    use Illuminate\Support\Facades\Cache;
     
    public function getCourse(int $id)
    {
        return Cache::remember("course:{$id}", 3600, function () use ($id) {
            return $this->courseRepository->findById($id);
        });
    }
  5. Filas e Jobs:

    • Use filas para processar tarefas pesadas de forma assíncrona.
    • Implemente jobs para encapsular lógica de processamento em background.
    namespace App\Modules\Courses\Jobs;
     
    use Illuminate\Bus\Queueable;
    use Illuminate\Contracts\Queue\ShouldQueue;
     
    class ProcessCourseVideo implements ShouldQueue
    {
        use Queueable;
     
        private $courseId;
     
        public function __construct(int $courseId)
        {
            $this->courseId = $courseId;
        }
     
        public function handle()
        {
            // Lógica de processamento de vídeo
        }
    }
  6. Eventos e Listeners:

    • Use eventos para desacoplar partes do sistema e permitir extensibilidade.
    namespace App\Modules\Courses\Events;
     
    use App\Modules\Courses\Models\Course;
    use Illuminate\Foundation\Events\Dispatchable;
     
    class CourseCreated
    {
        use Dispatchable;
     
        public $course;
     
        public function __construct(Course $course)
        {
            $this->course = $course;
        }
    }
  7. API interna:

    • Crie uma API interna bem definida para facilitar futuras migrações para microserviços, se necessário.
  8. Testes:

    • Implemente testes unitários, de integração e end-to-end.
    • Use factories e seeders para facilitar a criação de dados de teste.
    namespace Tests\Unit\Modules\Courses;
     
    use Tests\TestCase;
    use App\Modules\Courses\Services\CourseService;
    use App\Modules\Courses\Repositories\CourseRepository;
    use Mockery;
     
    class CourseServiceTest extends TestCase
    {
        public function testCreateCourse()
        {
            $mockRepo = Mockery::mock(CourseRepository::class);
            $mockRepo->shouldReceive('create')->once()->andReturn(new Course());
     
            $service = new CourseService($mockRepo);
            $result = $service->createCourse(['title' => 'Test Course']);
     
            $this->assertInstanceOf(Course::class, $result);
        }
    }

2.3. Microserviços (produto de AI)

O Toolzz AI é construído usando uma arquitetura de microserviços, que oferece maior flexibilidade e escalabilidade para este produto complexo e em rápida evolução.

Características principais:

  • Serviços independentes: Cada funcionalidade principal é um serviço separado.
  • Bancos de dados dedicados: Cada serviço tem seu próprio banco de dados.
  • Comunicação via API: Os serviços se comunicam através de APIs REST ou gRPC.

Boas práticas para microserviços:

  1. Design de API:

    • Use versionamento de API (ex: /api/v1/process-text).
    • Implemente rate limiting e autenticação consistente em todos os serviços.
  2. Service Discovery:

    • Use um sistema de service discovery como Consul ou etcd.
  3. Resiliência:

    • Implemente circuit breakers para prevenir falhas em cascata.
    • Use retry policies para lidar com falhas temporárias.
    const circuitBreaker = new CircuitBreaker({
      failureThreshold: 3,
      successThreshold: 2,
      timeout: 10000
    });
     
    circuitBreaker.fire(async () => {
      const result = await someExternalService.call();
      return result;
    }).then(console.log).catch(console.error);
  4. Logging centralizado:

    • Use uma solução de logging centralizado como ELK stack (Elasticsearch, Logstash, Kibana).
  5. Monitoramento:

    • Implemente health checks em todos os serviços.
    • Use ferramentas como Prometheus e Grafana para monitoramento e alertas.
  6. CI/CD:

    • Implemente pipelines de CI/CD para cada serviço.
    • Use containers (Docker) para garantir consistência entre ambientes.
    # Exemplo de Dockerfile
    FROM node:14
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 8080
    CMD [ "node", "server.js" ]
  7. Segurança:

    • Implemente autenticação e autorização em cada serviço.
    • Use HTTPS para todas as comunicações.
  8. Gerenciamento de configuração:

    • Use um serviço de configuração centralizado.
    • Nunca armazene segredos no código ou em repositórios.
    const config = require('config');
    const dbConfig = config.get('Customer.dbConfig');
  9. Testes:

    • Implemente testes unitários e de integração para cada serviço.
    • Use testes de contrato para garantir compatibilidade entre serviços.
    const { Pact } = require('@pact-foundation/pact');
    const { somethingLike } = Pact.Matchers;
     
    const provider = new Pact({
      consumer: 'MyConsumer',
      provider: 'MyProvider',
    });
     
    describe('API Pact test', () => {
      it('should return user data', async () => {
        await provider.addInteraction({
          state: 'a user exists',
          uponReceiving: 'a request for user data',
          withRequest: {
            method: 'GET',
            path: '/api/user/1',
          },
          willRespondWith: {
            status: 200,
            body: somethingLike({
              id: 1,
              name: 'John Doe',
            }),
          },
        });
     
        // Run API client test
      });
    });

2.4. Outras arquiteturas específicas por produto

Toolzz Bots (Monolito com componentes event-driven)

  1. Arquitetura principal: Monolito Node.js
  2. Componentes event-driven: Para processamento de mensagens e atualizações de estado do bot

Práticas específicas:

  • Use um message broker como RabbitMQ para gerenciar eventos.
  • Implemente o padrão CQRS (Command Query Responsibility Segregation) para separar operações de leitura e escrita.
// Exemplo de produtor de evento
const amqp = require('amqplib');
 
async function publishEvent(eventType, data) {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();
  const exchange = 'bot_events';
  
  await channel.assertExchange(exchange, 'topic', { durable: false });
  channel.publish(exchange, eventType, Buffer.from(JSON.stringify(data)));
  
  console.log(`Sent ${eventType}`);
  
  setTimeout(() => {
    connection.close();
  }, 500);
}
 
publishEvent('message.received', { botId: '123', message: 'Hello' });

Toolzz Chat (Microserviços com componentes real-time)

  1. Arquitetura principal: Microserviços Ruby on Rails e Node.js
  2. Componentes real-time: WebSockets para comunicação em tempo real

Práticas específicas:

  • Use Redis para gerenciamento de sessões e pub/sub para WebSockets.
  • Implemente uma API Gateway para rotear requisições e gerenciar autenticação.
# Exemplo de ActionCable em Rails para WebSocket
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room_id]}"
  end
 
  def receive(data)
    ActionCable.server.broadcast("chat_#{params[:room_id]}", data)
  end
end

Toolzz Connect (Arquitetura Serverless com componentes de microserviços)

  1. Arquitetura principal: Serverless usando AWS Lambda
  2. Componentes de microserviços: Para funcionalidades complexas ou que requerem estado

Práticas específicas:

  • Use Step Functions para orquestrar fluxos de trabalho complexos.
  • Implemente DynamoDB para armazenamento de dados com alta performance.
// Exemplo de função Lambda
exports.handler = async (event) => {
    const dynamodb = new AWS.DynamoDB.DocumentClient();
    const { id } = event.pathParameters;
 
    const params = {
        TableName: "Users",
        Key: { id }
    };
 
    try {
        const result = await dynamodb.get(params).promise();
        return {
            statusCode: 200,
            body: JSON.stringify(result.Item)
        };
    } catch (error) {
        console.log(error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: "Could not retrieve user" })
        };
    }
};

2.5. Princípios de design de arquitetura

Independentemente da arquitetura específica, todos os nossos produtos devem aderir aos seguintes princípios de design:

  1. Separação de Responsabilidades: Cada componente deve ter uma responsabilidade bem definida e única.

  2. DRY (Don't Repeat Yourself): Evite duplicação de código através de abstração e modularização.

  3. SOLID:

    • Single Responsibility Principle
    • Open/Closed Principle
    • Liskov Substitution Principle
    • Interface Segregation Principle
    • Dependency Inversion Principle
  4. Princípio de Menor Privilégio: Componentes devem ter acesso apenas aos recursos que são estritamente necessários.

  5. Design para Falhas: Assuma que falhas ocorrerão e projete sistemas para serem resilientes.

  6. Observabilidade: Sistemas devem ser projetados para serem facilmente monitorados e debugados.

  7. Segurança por Design: Considere aspectos de segurança desde o início do design da arquitetura.

  8. Escalabilidade Horizontal: Prefira designs que permitam escalar adicionando mais instâncias em vez de aumentar recursos de instâncias existentes.

  9. Consistência Eventual: Em sistemas distribuídos, aceite consistência eventual quando apropriado para melhorar a disponibilidade e performance.

  10. API First: Projete APIs antes de implementar funcionalidades, facilitando integrações futuras.

Exemplo de aplicação do princípio SOLID (Single Responsibility):

// Antes
class User
{
    public function create($data)
    {
        // Lógica para criar usuário
    }
 
    public function sendWelcomeEmail($user)
    {
        // Lógica para enviar e-mail
    }
}
 
// Depois
class User
{
    public function create($data)
    {
        // Lógica para criar usuário
    }
}
 
class EmailService
{
    public function sendWelcomeEmail($user)
    {
        // Lógica para enviar e-mail
    }
}

Ao seguir estes princípios e práticas específicas para cada arquitetura, garantimos que nossos sistemas sejam robustos, escaláveis e mantenham-se alinhados com as melhores práticas da indústria.