MSC (Model-Service-Controller) é um padrão útil para manter a lógica de negócio separada do tratamento HTTP e do acesso a dados. O model é dono do schema. O controller é dono da request/response. O service é dono de tudo que está no meio. O que o padrão não diz é como organizar a própria camada de serviços. Você deve criar um serviço por model? Agrupar serviços por domínio ou objetivo? Usar uma mistura? Essa pergunta tem uma resposta concreta, e ela depende do que a sua aplicação realmente faz.
Entendendo o MSC no Ecossistema de Software Moderno
Na arquitetura MSC:
- Models encapsulam schemas de dados e objetos de negócio.
- Services contêm a lógica de negócio ou de domínio que opera sobre os models.
- Controllers tratam requisições de entrada, chamam os serviços e retornam respostas.
A camada de serviços é onde as decisões de design reais residem. Uma camada de serviços bem estruturada mantém o código navegável à medida que a aplicação cresce, impede que a lógica vaze para os controllers e torna os serviços individualmente testáveis de forma isolada. O TypeScript adiciona tipagem forte, interfaces e generics que tornam essas fronteiras mais fáceis de impor e mais difíceis de violar acidentalmente.
O Desafio de Estruturar Serviços
Existem três abordagens principais, cada uma com um tradeoff diferente.
Um serviço por model é fácil de visualizar. UserService cuida de usuários, ProductService cuida de produtos, OrderService cuida de pedidos. Para aplicações pequenas ou operações CRUD simples, este é o ponto de partida correto. O risco é que lógica transversal — como enviar um e-mail de confirmação após um pedido ser realizado — acabe duplicada entre serviços ou seja encaixada de forma forçada em qualquer serviço que trate a ação disparadora.
Serviços organizados por objetivo agrupa a lógica em torno de operações de domínio, e não de entidades de dados. Em vez de um ProductService e um OrderService separados, você pode ter um CheckoutService que trata todo o fluxo de compra. Isso se alinha bem com a forma como o negócio pensa sobre suas operações e mantém etapas relacionadas próximas umas das outras. O risco é que um serviço de domínio pode absorver responsabilidades demais se seu escopo não estiver claramente definido, tornando-se tão emaranhado quanto o controller que deveria substituir.
Uma abordagem híbrida usa serviços centrados em models para CRUD direto e serviços centrados em domínio para operações que abrangem múltiplos models. Um UserService trata a criação e recuperação de usuários. Um AnalyticsService agrega dados de múltiplos serviços de model para produzir relatórios. Esta é a abordagem que escala melhor, mas exige julgamento contínuo sobre onde traçar as linhas.
Um Serviço por Model: Prós e Contras
As principais vantagens são simplicidade e descobribilidade. Todo desenvolvedor sabe imediatamente onde procurar a lógica relacionada a usuários. Novos membros da equipe conseguem se orientar rapidamente. Para aplicações menores ou microsserviços individuais com responsabilidades bem delimitadas, essa abordagem não apresenta desvantagens significativas.
Os problemas aparecem quando operações abrangem múltiplos models. Notificações por e-mail, cálculos de impostos, log de auditoria e outras preocupações transversais acabam duplicadas em cada serviço que as precisa, ou se tornam métodos no serviço que pareceu mais conveniente no momento. Nenhum dos dois resultados é limpo.
Um serviço centrado em model em TypeScript:
// models/User.ts
export interface User {
id: string;
name: string;
email: string;
}
// services/UserService.ts
import { User } from '../models/User';
export class UserService {
private users: User[] = [];
public createUser(userData: Omit<User, 'id'>): User {
const newUser: User = {
id: Math.random().toString(36).substring(2),
...userData
};
this.users.push(newUser);
return newUser;
}
public getUserById(userId: string): User | undefined {
return this.users.find(user => user.id === userId);
}
// Lógica CRUD adicional, se necessário
}
UserService tem um único trabalho: gerenciar dados de usuário. É só isso que ele deve fazer.
Serviços Orientados a Objetivos: Adotando o Domain-Driven Design
Serviços orientados a objetivos fazem sentido quando as operações relevantes do seu sistema envolvem naturalmente múltiplos models trabalhando juntos. Um checkout de e-commerce envolve um usuário, um pedido, um pagamento e um e-mail. Dividir essa lógica entre quatro serviços de model separados significa que o fluxo de checkout vive em quatro lugares, o que torna mais difícil entendê-lo e alterá-lo.
// domain/EcommerceService.ts
import { User } from '../models/User';
import { Order } from '../models/Order';
import { EmailService } from './EmailService';
import { PaymentGateway } from './PaymentGateway';
export class EcommerceService {
constructor(
private emailService: EmailService,
private paymentGateway: PaymentGateway
) {}
public async processOrder(user: User, order: Order): Promise<void> {
// Executar pagamento
if (await this.paymentGateway.charge(user, order.total)) {
// Salvar pedido no banco de dados ou store em memória
// ...alguma lógica aqui
// Enviar e-mail de confirmação
await this.emailService.sendEmail(user.email, 'Order Confirmed', 'Your order has been processed successfully!');
}
}
}
EcommerceService orquestra a sequência completa do checkout. EmailService e PaymentGateway são injetados como dependências, mantendo sua lógica separada e testável. O serviço de domínio permanece focado na orquestração, não nos detalhes de implementação do pagamento ou do e-mail.
O risco a observar: um serviço que começa tratando pedidos, depois absorve a lógica do catálogo de produtos, depois o gerenciamento de estoque, depois as devoluções. Nesse ponto, ele deixa de ser um serviço de domínio focado; torna-se um acumulador genérico que precisa ser dividido.
A Rota Híbrida: O Melhor dos Dois Mundos
A abordagem híbrida usa serviços de model para CRUD direto e serviços de domínio para operações transversais. Os serviços de model tratam entidades individuais de forma limpa. Os serviços de domínio chamam os serviços de model para obter dados e, em seguida, executam operações de nível mais alto sobre eles.
O AnalyticsService abaixo é um bom exemplo: ele não possui dados diretamente, mas puxa dados de múltiplos serviços de model para calcular métricas agregadas.
// services/AnalyticsService.ts
import { UserService } from './UserService';
import { OrderService } from './OrderService';
export class AnalyticsService {
constructor(
private userService: UserService,
private orderService: OrderService
) {}
public async getAverageSpendingPerUser(): Promise<number> {
const users = this.userService.getAllUsers();
let totalSpending = 0;
for (const user of users) {
const orders = this.orderService.getOrdersByUserId(user.id);
const userSpending = orders
.map(order => order.total)
.reduce((acc, cur) => acc + cur, 0);
totalSpending += userSpending;
}
return totalSpending / users.length;
}
}
AnalyticsService tem fronteiras claras: lê dados através dos serviços existentes e calcula um resultado. Ele não cria nem atualiza nada. Essa clareza torna-o simples de testar e fácil de raciocinar.
Dicas para Evoluir sua Arquitetura MSC
Comece com um serviço por model e divida apenas quando um serviço estiver fazendo coisas demais ou quando lógica transversal estiver sendo duplicada. A abstração prematura em serviços de domínio cria complexidade antes que você saiba o que o domínio realmente precisa.
O sistema de tipos do TypeScript ajuda a impor as fronteiras dos serviços. Defina interfaces para o que cada serviço aceita e retorna. Use classes abstratas ou interfaces para tornar os pontos de injeção explícitos. Generics podem reduzir o boilerplate para serviços que fazem coisas semelhantes em diferentes tipos de model.
Em arquiteturas serverless, cada função Lambda frequentemente mapeia diretamente para uma operação de serviço. A mesma disciplina de fronteira de serviço se aplica: cada função deve ter um trabalho claro. Message brokers (AWS SNS/SQS, RabbitMQ, Kafka) tratam a comunicação entre serviços sem acoplamento forte.
Quando um serviço de domínio fica grande, quebre-o de forma incremental. Extraia uma responsabilidade de cada vez, verifique que nada quebra, então continue. Uma reorganização em grande escala feita de uma só vez é mais difícil de revisar e tem maior probabilidade de introduzir bugs.
Por Que a Organização Adequada dos Serviços Importa
À medida que uma aplicação escala, a camada de serviços é onde a maior parte da complexidade se acumula. Uma organização ruim de serviços leva a lógica duplicada, propriedade pouco clara e serviços difíceis de testar porque dependem de muitas coisas. Uma boa organização significa que cada serviço tem um trabalho claro, suas dependências são explícitas e os testes para ele não exigem que toda a aplicação esteja rodando.
Para equipes, fronteiras claras de serviço reduzem a sobrecarga de coordenação. Desenvolvedores front-end, engenheiros backend e DevOps podem trabalhar em diferentes partes da base de código sem ficar constantemente pisando uns nos outros. O onboarding é mais rápido porque a estrutura diz onde as coisas pertencem.
O padrão também se paga durante a depuração. Quando algo dá errado em produção, uma camada de serviços bem organizada torna muito mais fácil rastrear qual serviço é responsável pela falha e com quais dados ele estava trabalhando.
Conclusão
A organização correta dos serviços depende do que a sua aplicação faz. Um serviço por model é o ponto de partida correto para a maioria das aplicações. Adicione serviços orientados a domínio quando operações consistentemente abrangem múltiplos models e o código para essas operações continua se espalhando. A abordagem híbrida é o padrão prático para aplicações de tamanho significativo.
Qualquer que seja a estrutura que você escolher, mantenha as fronteiras dos serviços explícitas, injete dependências em vez de importá-las diretamente e não espere até que um serviço esteja uma bagunça para dividi-lo. Quanto antes você perceber que um serviço está acumulando responsabilidades que não deveria ter, mais fácil será a refatoração.