Najlepsze Pytania o Wzorce i Architekturę - Java, Spring, Node.js, Express
W tym artykule zebrałem najlepsze pytania o wzorce projektowe i architekturę z czterech technologii backend: Java, Spring Framework, Node.js i Express.js. To pytania, które sam zadaję kandydatom i które słyszałem na rozmowach w firmach od startupów po korporacje. Każde pytanie ma dwie wersje odpowiedzi - krótką na 30 sekund (gdy rekruter chce szybko sprawdzić podstawy) i rozszerzoną na 2 minuty (gdy chce zagłębić się w temat).
Fundamenty: Inversion of Control i Dependency Injection
Czym jest IoC i DI? Jaka jest między nimi różnica?
Odpowiedź w 30 sekund:
Inversion of Control to zasada projektowa, gdzie kontrola nad tworzeniem i zarządzaniem obiektami jest przenoszona z kodu aplikacji do zewnętrznego kontenera. Dependency Injection to konkretna implementacja IoC - kontener wstrzykuje zależności do obiektu przez konstruktor, setter lub pole. IoC to idea, DI to technika jej realizacji.
Odpowiedź w 2 minuty:
Wyobraź sobie tradycyjny kod, gdzie klasa OrderService sama tworzy instancję PaymentGateway:
// Bez IoC - klasa sama zarządza zależnościami
public class OrderService {
// Silne powiązanie - trudne do testowania
private PaymentGateway paymentGateway = new StripePaymentGateway();
public void processOrder(Order order) {
paymentGateway.charge(order.getTotal());
}
}
Problem? Nie możesz łatwo podmienić StripePaymentGateway na PayPalPaymentGateway w testach. Nie możesz też zmienić implementacji bez modyfikacji OrderService.
Inversion of Control odwraca tę kontrolę - to nie klasa decyduje jakich zależności użyje, tylko zewnętrzny kontener dostarcza jej gotowe obiekty:
// Z DI - zależność wstrzykiwana przez konstruktor
public class OrderService {
private final PaymentGateway paymentGateway;
// Konstruktor przyjmuje interfejs, nie konkretną implementację
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void processOrder(Order order) {
paymentGateway.charge(order.getTotal());
}
}
Teraz OrderService nie wie i nie dba, czy dostaje Stripe czy PayPal - zależy tylko od interfejsu. W testach możesz wstrzyknąć mock:
// Łatwe testowanie z mockiem
@Test
void shouldChargeCorrectAmount() {
PaymentGateway mockGateway = mock(PaymentGateway.class);
OrderService service = new OrderService(mockGateway);
service.processOrder(new Order(100.0));
verify(mockGateway).charge(100.0);
}
W Spring kontener IoC (ApplicationContext) automatycznie wykrywa zależności i je wstrzykuje. W Node.js możesz użyć bibliotek jak InversifyJS lub stosować DI ręcznie przez konstruktory.
Spring Framework: Architektura i Wzorce
Jak działa kontener IoC w Spring i jakie są różnice między BeanFactory a ApplicationContext?
Odpowiedź w 30 sekund:
BeanFactory to podstawowy kontener IoC - tworzy i zarządza beanami, wspiera lazy loading. ApplicationContext rozszerza BeanFactory o: eager loading domyślnie, obsługę zdarzeń, internacjonalizację (i18n), integrację z AOP. W praktyce zawsze używamy ApplicationContext - BeanFactory jest zbyt podstawowy dla realnych aplikacji.
Odpowiedź w 2 minuty:
BeanFactory to interfejs definiujący podstawowe operacje kontenera: getBean(), sprawdzanie czy bean istnieje, pobieranie typu. To minimum potrzebne do Dependency Injection:
// BeanFactory - podstawowe API
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
MyService service = factory.getBean("myService", MyService.class);
ApplicationContext to "bogaty kuzyn" BeanFactory. Dziedziczy wszystkie jego możliwości i dodaje funkcje enterprise:
// ApplicationContext - pełny kontener Spring
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// Wszystko co BeanFactory plus:
// - Automatyczne wykrywanie @Component, @Service, @Repository
// - Obsługa zdarzeń aplikacji
// - Dostęp do zasobów (pliki, URL)
// - Internacjonalizacja (MessageSource)
// - Integracja z AOP
Kluczowa różnica w zachowaniu - domyślnie BeanFactory tworzy beany lazy (przy pierwszym użyciu), ApplicationContext tworzy singleton beany przy starcie. To pozwala wykryć błędy konfiguracji wcześniej:
// Przy starcie ApplicationContext - błąd zostanie wykryty od razu
// Przy BeanFactory - błąd dopiero gdy ktoś poprosi o tego beana
@Service
public class OrderService {
@Autowired
private PaymentGateway gateway; // Brak implementacji?
// ApplicationContext: błąd przy starcie
// BeanFactory: błąd przy pierwszym getBean()
}
W Spring Boot nigdy nie tworzysz kontenera ręcznie - @SpringBootApplication robi to za Ciebie, tworząc AnnotationConfigServletWebServerApplicationContext dla aplikacji webowych.
Porównaj adnotacje @Component, @Service, @Repository i @Controller
Odpowiedź w 30 sekund:
Wszystkie cztery są wykrywane przez component scanning i tworzą beany Spring. @Component to generyczna adnotacja. @Service oznacza warstwę logiki biznesowej. @Repository oznacza warstwę DAO i dodatkowo tłumaczy wyjątki bazodanowe. @Controller oznacza kontroler MVC obsługujący żądania HTTP. Różnią się semantyką i dodatkowymi funkcjami.
Odpowiedź w 2 minuty:
Wszystkie te adnotacje dziedziczą po @Component - Spring traktuje je jako "stereotypy" oznaczające różne warstwy aplikacji:
// Ogólny komponent - gdy nie pasuje do żadnej warstwy
@Component
public class EmailValidator {
public boolean isValid(String email) {
return email.contains("@");
}
}
// Warstwa logiki biznesowej
@Service
public class OrderService {
private final OrderRepository repository;
private final PaymentGateway gateway;
public Order createOrder(OrderRequest request) {
// Logika biznesowa
}
}
// Warstwa dostępu do danych
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager em;
public Order findById(Long id) {
return em.find(Order.class, id);
}
}
// Warstwa prezentacji (kontroler HTTP)
@Controller
public class OrderController {
@GetMapping("/orders/{id}")
public String showOrder(@PathVariable Long id, Model model) {
model.addAttribute("order", orderService.findById(id));
return "order-details"; // nazwa widoku
}
}
@Repository ma specjalną funkcję - automatycznie tłumaczy wyjątki specyficzne dla bazy danych (SQLException, HibernateException) na ujednoliconą hierarchię DataAccessException Spring:
@Repository
public class JdbcUserRepository {
public User findById(Long id) {
// SQLException zostanie automatycznie przetłumaczony na
// DataAccessException bez dodatkowego kodu
}
}
@RestController to @Controller + @ResponseBody - metody zwracają dane bezpośrednio (JSON/XML) zamiast nazwy widoku:
@RestController
@RequestMapping("/api")
public class OrderApiController {
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable Long id) {
return orderService.findById(id); // Automatyczna serializacja do JSON
}
}
(bazowy)"] Service["@Service
(logika biznesowa)"] Repository["@Repository
(dostęp do danych)
+ tłumaczenie wyjątków"] Controller["@Controller
(warstwa web)
+ obsługa żądań HTTP"] RestController["@RestController
= @Controller + @ResponseBody"] Component --> Service Component --> Repository Component --> Controller Controller --> RestController end style Component fill:#e3f2fd style Service fill:#fff3e0 style Repository fill:#e8f5e9 style Controller fill:#fce4ec style RestController fill:#f3e5f5
Jak działa @Transactional i jakie są pułapki?
Odpowiedź w 30 sekund:
@Transactional oznacza metodę lub klasę jako transakcyjną - Spring automatycznie rozpoczyna transakcję przed wywołaniem i commituje po zakończeniu (lub rollback przy wyjątku). Działa przez proxy AOP. Główne pułapki: nie działa dla wywołań wewnętrznych (self-invocation), domyślnie rollback tylko dla RuntimeException, proxy nie przechwytuje prywatnych metod.
Odpowiedź w 2 minuty:
Spring tworzy proxy wokół klasy z @Transactional. Gdy wywołujesz metodę transakcyjną, proxy:
- Otwiera transakcję
- Wywołuje oryginalną metodę
- Commituje (sukces) lub rollback (wyjątek)
@Service
public class OrderService {
@Transactional
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
orderRepository.save(order);
// Jeśli tu poleci RuntimeException - cała transakcja zostanie wycofana
paymentService.charge(order);
return order;
}
}
Tu robi się ciekawie - pułapka numer jeden, self-invocation:
@Service
public class OrderService {
public void processOrders(List<OrderRequest> requests) {
for (OrderRequest req : requests) {
createOrder(req); // NIE DZIAŁA! Wywołanie wewnętrzne omija proxy
}
}
@Transactional
public Order createOrder(OrderRequest request) {
// Ta transakcja NIE zostanie utworzona przy wywołaniu z processOrders
}
}
Rozwiązanie? Wstrzyknij serwis sam do siebie lub wydziel do osobnej klasy:
@Service
public class OrderService {
@Autowired
private OrderService self; // Wstrzyknięcie proxy samego siebie
public void processOrders(List<OrderRequest> requests) {
for (OrderRequest req : requests) {
self.createOrder(req); // Teraz przechodzi przez proxy
}
}
}
Pułapka numer dwa - checked exceptions nie powodują rollback domyślnie:
@Transactional
public void transfer(Account from, Account to, BigDecimal amount)
throws InsufficientFundsException { // Checked exception
from.debit(amount);
to.credit(amount);
if (from.getBalance().compareTo(BigDecimal.ZERO) < 0) {
throw new InsufficientFundsException(); // Transakcja NIE zostanie wycofana!
}
}
// Rozwiązanie - jawne określenie rollback
@Transactional(rollbackFor = InsufficientFundsException.class)
public void transfer(...) throws InsufficientFundsException {
// Teraz rollback nastąpi również dla tego wyjątku
}
Node.js: Event Loop i Architektura Asynchroniczna
Wyjaśnij jak działa Event Loop i dlaczego Node.js jest jednowątkowy
Odpowiedź w 30 sekund:
Event Loop to mechanizm zarządzający asynchronicznością w Node.js. Główny wątek wykonuje kod JavaScript, ale operacje I/O (baza, pliki, sieć) są delegowane do puli wątków libuv. Gdy operacja się kończy, callback trafia do kolejki. Event Loop pobiera callbacki gdy stos wywołań jest pusty. Jednowątkowość eliminuje problemy z synchronizacją, ale wymaga unikania blokujących operacji.
Odpowiedź w 2 minuty:
Node.js używa modelu "single-threaded event loop with non-blocking I/O". Brzmi skomplikowanie, ale idea jest prosta:
// Wyobraź sobie restaurację z jednym kelnerem (główny wątek)
// Kelner przyjmuje zamówienia (żądania HTTP)
// Kuchnia to pula wątków libuv (operacje I/O)
console.log('1. Kelner przyjmuje zamówienie'); // Synchroniczne - natychmiast
// Zamówienie idzie do kuchni (asynchroniczne I/O)
fs.readFile('menu.txt', (err, data) => {
console.log('3. Danie gotowe, kelner przynosi'); // Callback - później
});
console.log('2. Kelner idzie do następnego stolika'); // Synchroniczne - zaraz po 1
Output: 1, 2, 3 - nie 1, 3, 2. Kelner nie czeka przy kuchni.
Event Loop ma fazy:
setTimeout, setInterval"] Pending["2. Pending Callbacks
I/O callbacks"] Poll["3. Poll
Nowe I/O events"] Check["4. Check
setImmediate"] Close["5. Close Callbacks
socket.on('close')"] Timers --> Pending Pending --> Poll Poll --> Check Check --> Close Close --> Timers end MicroTasks["Microtasks Queue
process.nextTick, Promise.then"] MicroTasks -.->|"wykonywane między fazami"| Timers MicroTasks -.-> Pending MicroTasks -.-> Poll style Poll fill:#fff3e0 style MicroTasks fill:#e8f5e9
Pułapka - blokowanie Event Loop:
// ZŁY KOD - blokuje Event Loop
app.get('/hash', (req, res) => {
// Synchroniczne hashowanie - blokuje WSZYSTKIE żądania
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
res.send(hash);
});
// DOBRY KOD - nie blokuje
app.get('/hash', async (req, res) => {
// Asynchroniczne - idzie do puli wątków
const hash = await crypto.pbkdf2(password, salt, 100000, 64, 'sha512');
res.send(hash);
});
Zasada: operacje CPU-intensive (hashowanie, kompresja, obliczenia) przenieś do Worker Threads lub osobnego procesu.
Czym różni się CommonJS od ES Modules i kiedy używać którego?
Odpowiedź w 30 sekund:
CommonJS (require/module.exports) to tradycyjny system modułów Node.js - synchroniczny, dynamiczny (można require w runtime). ES Modules (import/export) to standard ECMAScript - asynchroniczny, statyczny (analizowany przed wykonaniem). CommonJS dla legacy kodu i prostych skryptów. ESM dla nowych projektów - lepsze tree-shaking, top-level await, kompatybilność z przeglądarkami.
Odpowiedź w 2 minuty:
// CommonJS - tradycyjny Node.js
const express = require('express');
const { readFile } = require('fs');
module.exports = {
startServer: function() { /* ... */ }
};
// Można dynamicznie
if (process.env.USE_CACHE) {
const cache = require('./cache'); // Warunkowy import
}
// ES Modules - nowoczesny standard
import express from 'express';
import { readFile } from 'fs/promises';
export function startServer() { /* ... */ }
// Dynamiczny import (zwraca Promise)
if (process.env.USE_CACHE) {
const { cache } = await import('./cache.js');
}
Kluczowe różnice:
| Aspekt | CommonJS | ES Modules |
|---|---|---|
| Ładowanie | Synchroniczne | Asynchroniczne |
| Analiza | Runtime | Przed wykonaniem (static) |
| Top-level await | Nie | Tak |
this w module |
exports |
undefined |
| Rozszerzenie |
.js (domyślne) |
.mjs lub "type": "module"
|
| Tree-shaking | Słabe | Dobre |
Kiedy ESM? Nowe projekty, biblioteki publikowane na npm, kod współdzielony z frontendem.
Kiedy CommonJS? Legacy projekty, skrypty CLI, gdy używasz bibliotek tylko-CommonJS.
// package.json dla ESM
{
"type": "module",
"exports": {
".": "./src/index.js"
}
}
Express.js: Middleware i Architektura Aplikacji
Czym jest middleware i jak zorganizować go w dużej aplikacji?
Odpowiedź w 30 sekund:
Middleware to funkcja z dostępem do req, res i next. Przetwarza żądanie, może je zmodyfikować, zakończyć odpowiedzią lub przekazać dalej. W dużej aplikacji: podziel na moduły (auth, validation, errorHandler), używaj Router dla grupowania tras, globalny error handler na końcu, zachowaj właściwą kolejność (parsery -> auth -> routes -> errors).
Odpowiedź w 2 minuty:
Middleware to serce Express. Każde żądanie przechodzi przez łańcuch funkcji:
// Anatomia middleware
function myMiddleware(req, res, next) {
// 1. Zrób coś z req/res
console.log(`${req.method} ${req.url}`);
// 2. Albo zakończ odpowiedzią
// res.status(401).send('Unauthorized');
// 3. Albo przekaż dalej
next();
// 4. Lub przekaż błąd
// next(new Error('Something went wrong'));
}
Dla dużej aplikacji - struktura katalogów:
src/
├── middleware/
│ ├── auth.js # Weryfikacja JWT/sesji
│ ├── validation.js # Walidacja body
│ ├── rateLimit.js # Ograniczanie requestów
│ └── errorHandler.js # Centralny handler błędów
├── routes/
│ ├── index.js # Agreguje wszystkie routery
│ ├── users.js # /api/users/*
│ └── orders.js # /api/orders/*
├── controllers/
│ ├── userController.js
│ └── orderController.js
└── app.js # Konfiguracja Express
// app.js - właściwa kolejność middleware
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const authMiddleware = require('./middleware/auth');
const errorHandler = require('./middleware/errorHandler');
const routes = require('./routes');
const app = express();
// 1. Security middleware (najpierw!)
app.use(helmet());
app.use(cors());
// 2. Parsery body
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 3. Logging
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
});
// 4. Routes (z auth gdzie potrzeba)
app.use('/api', routes);
// 5. 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
// 6. Error handler (ZAWSZE ostatni!)
app.use(errorHandler);
// middleware/errorHandler.js - centralny handler błędów
function errorHandler(err, req, res, next) {
console.error(err.stack);
// Rozróżnianie typów błędów
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation Error',
details: err.details
});
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Invalid token' });
}
// Domyślny błąd serwera
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: err.message
});
}
module.exports = errorHandler;
Jak zaimplementować Dependency Injection w Express bez frameworka?
Odpowiedź w 30 sekund:
Bez frameworka IoC używaj constructor injection: twórz instancje serwisów w jednym miejscu (composition root), wstrzykuj przez konstruktory kontrolerów/route handlerów. Alternatywnie: factory functions zwracające skonfigurowane handlery. Unikaj globalnych singletonów - utrudniają testowanie.
Odpowiedź w 2 minuty:
Express nie wymusza DI, ale możesz go zaimplementować ręcznie:
// services/userService.js
class UserService {
constructor(userRepository, emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
async createUser(data) {
const user = await this.userRepository.create(data);
await this.emailService.sendWelcome(user.email);
return user;
}
}
module.exports = UserService;
// controllers/userController.js - factory function
function createUserController(userService) {
return {
async create(req, res, next) {
try {
const user = await userService.createUser(req.body);
res.status(201).json(user);
} catch (err) {
next(err);
}
},
async getById(req, res, next) {
try {
const user = await userService.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
} catch (err) {
next(err);
}
}
};
}
module.exports = createUserController;
// composition-root.js - tutaj składamy wszystko
const UserRepository = require('./repositories/userRepository');
const EmailService = require('./services/emailService');
const UserService = require('./services/userService');
const createUserController = require('./controllers/userController');
const createUserRouter = require('./routes/users');
function createContainer(config) {
// Tworzymy instancje w odpowiedniej kolejności
const userRepository = new UserRepository(config.db);
const emailService = new EmailService(config.smtp);
const userService = new UserService(userRepository, emailService);
const userController = createUserController(userService);
const userRouter = createUserRouter(userController);
return {
userRouter,
// Expose dla testów
userService,
userRepository
};
}
module.exports = createContainer;
// app.js
const express = require('express');
const createContainer = require('./composition-root');
const config = require('./config');
const app = express();
const container = createContainer(config);
app.use(express.json());
app.use('/api/users', container.userRouter);
module.exports = app;
Teraz testy są proste:
// tests/userService.test.js
const UserService = require('../services/userService');
describe('UserService', () => {
it('should send welcome email after creating user', async () => {
// Mock dependencies
const mockRepo = { create: jest.fn().mockResolvedValue({ id: 1, email: 'test@test.com' }) };
const mockEmail = { sendWelcome: jest.fn().mockResolvedValue() };
const service = new UserService(mockRepo, mockEmail);
await service.createUser({ email: 'test@test.com' });
expect(mockEmail.sendWelcome).toHaveBeenCalledWith('test@test.com');
});
});
Wzorce Projektowe w Praktyce
Kompozycja vs Dziedziczenie - kiedy używać którego?
Odpowiedź w 30 sekund:
Dziedziczenie dla relacji "jest" (is-a): Dog jest Animal. Kompozycja dla relacji "ma" (has-a): Car ma Engine. Preferuj kompozycję - daje elastyczność runtime, unika problemów z głęboką hierarchią, ułatwia testowanie. Zasada Gang of Four: "Favor composition over inheritance".
Odpowiedź w 2 minuty:
// Dziedziczenie - gdy naprawdę "jest" (is-a)
public abstract class Animal {
protected String name;
public abstract void makeSound();
public void sleep() {
System.out.println(name + " śpi");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Hau hau!");
}
public void fetch() {
System.out.println(name + " przynosi piłkę");
}
}
// Dog JEST Animal - to ma sens
Animal pet = new Dog();
pet.makeSound(); // Polimorfizm działa
Problem z dziedziczeniem - co jeśli potrzebujesz "pływającego psa" i "latającego psa"?
// Problemy z dziedziczeniem
public class FlyingDog extends Dog { /* jak odziedziczyć latanie? */ }
public class SwimmingDog extends Dog { /* a pływanie? */ }
public class FlyingSwimmingDog extends ??? { /* Java nie ma wielodziedziczenia! */ }
Rozwiązanie - kompozycja z interfejsami:
// Kompozycja - elastyczność przez składanie
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
public class Dog {
private String name;
private Flyable flyBehavior; // Może być null
private Swimmable swimBehavior; // Może być null
public Dog(String name) {
this.name = name;
}
public void setFlyBehavior(Flyable flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void performFly() {
if (flyBehavior != null) {
flyBehavior.fly();
} else {
System.out.println(name + " nie umie latać");
}
}
}
// Implementacje zachowań
public class JetpackFlying implements Flyable {
public void fly() {
System.out.println("Leci z jetpackiem!");
}
}
// Użycie
Dog superDog = new Dog("Rex");
superDog.setFlyBehavior(new JetpackFlying()); // Zmiana w runtime!
superDog.performFly();
W Node.js/JavaScript - preferuj kompozycję obiektów:
// Kompozycja przez mixiny/Object.assign
const canFly = {
fly() {
console.log(`${this.name} leci!`);
}
};
const canSwim = {
swim() {
console.log(`${this.name} pływa!`);
}
};
function createDog(name) {
return {
name,
bark() {
console.log('Hau hau!');
}
};
}
// Składamy super-psa
const superDog = Object.assign(
createDog('Rex'),
canFly,
canSwim
);
superDog.bark(); // Hau hau!
superDog.fly(); // Rex leci!
superDog.swim(); // Rex pływa!
Jak zaimplementować wzorzec Repository w Spring i Express?
Odpowiedź w 30 sekund:
Repository to warstwa abstrakcji nad dostępem do danych. W Spring: interfejs rozszerzający JpaRepository/CrudRepository - Spring generuje implementację. W Express: klasa/moduł enkapsulujący operacje na bazie, wstrzykiwany do serwisów. Cel: oddzielenie logiki biznesowej od szczegółów persystencji, łatwiejsze testowanie i podmiana źródła danych.
Odpowiedź w 2 minuty:
Spring Data JPA - minimalna implementacja:
// Interfejs - Spring generuje implementację
public interface OrderRepository extends JpaRepository<Order, Long> {
// Query Methods - Spring generuje zapytanie z nazwy
List<Order> findByStatus(OrderStatus status);
List<Order> findByCustomerIdOrderByCreatedAtDesc(Long customerId);
// Custom query gdy nazwa byłaby za długa
@Query("SELECT o FROM Order o WHERE o.total > :minTotal AND o.status = :status")
List<Order> findExpensiveOrdersByStatus(
@Param("minTotal") BigDecimal minTotal,
@Param("status") OrderStatus status
);
}
// Użycie w serwisie
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> getCustomerOrders(Long customerId) {
return orderRepository.findByCustomerIdOrderByCreatedAtDesc(customerId);
}
}
Express z czystym JavaScript:
// repositories/orderRepository.js
class OrderRepository {
constructor(db) {
this.db = db; // Instancja połączenia z bazą (Knex, Prisma, Mongoose...)
}
async findById(id) {
return this.db('orders').where({ id }).first();
}
async findByCustomerId(customerId) {
return this.db('orders')
.where({ customer_id: customerId })
.orderBy('created_at', 'desc');
}
async create(orderData) {
const [id] = await this.db('orders').insert(orderData);
return this.findById(id);
}
async updateStatus(id, status) {
await this.db('orders').where({ id }).update({ status });
return this.findById(id);
}
}
module.exports = OrderRepository;
// services/orderService.js
class OrderService {
constructor(orderRepository, paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
async createOrder(customerId, items) {
const total = this.calculateTotal(items);
const order = await this.orderRepository.create({
customer_id: customerId,
items: JSON.stringify(items),
total,
status: 'pending'
});
return order;
}
async getCustomerOrders(customerId) {
return this.orderRepository.findByCustomerId(customerId);
}
}
module.exports = OrderService;
Korzyść - łatwe mockowanie w testach:
// tests/orderService.test.js
describe('OrderService', () => {
it('should create order with correct total', async () => {
const mockRepo = {
create: jest.fn().mockResolvedValue({ id: 1, total: 150 }),
findById: jest.fn()
};
const service = new OrderService(mockRepo, {});
await service.createOrder(1, [
{ productId: 1, price: 100 },
{ productId: 2, price: 50 }
]);
expect(mockRepo.create).toHaveBeenCalledWith(
expect.objectContaining({ total: 150 })
);
});
});
Na Co Rekruterzy Naprawdę Zwracają Uwagę
Po przeprowadzeniu setek rozmów rekrutacyjnych na stanowiska backend, zauważyłem wzorce w odpowiedziach, które wyróżniają kandydatów:
1. Rozumienie "dlaczego", nie tylko "jak"
Słaba odpowiedź: "Używamy @Transactional żeby mieć transakcje." Dobra odpowiedź: "Używamy @Transactional, bo dzięki proxy AOP możemy deklaratywnie zarządzać transakcjami bez boilerplate'u. Ważne jest zrozumienie, że nie działa to dla wywołań wewnętrznych, bo omijają proxy."
2. Świadomość trade-offów
Słaba odpowiedź: "Kompozycja jest lepsza od dziedziczenia." Dobra odpowiedź: "Kompozycja daje elastyczność i ułatwia testowanie, ale dziedziczenie jest naturalne dla hierarchii 'jest' - Dog jest Animal. Problem zaczyna się przy głębokich hierarchiach i potrzebie wielokrotnego dziedziczenia. Preferuję kompozycję, ale nie dogmatycznie."
3. Praktyczne doświadczenie z problemami
Kandydat, który powie "Kiedyś spędziłem dzień debugując dlaczego @Transactional nie działało - okazało się, że metoda była wywoływana wewnętrznie i omijała proxy" - pokazuje realne doświadczenie, nie tylko teorię.
4. Umiejętność wyjaśniania prostym językiem
Najlepsi seniorzy potrafią wyjaśnić Event Loop w Node.js używając analogii do restauracji z jednym kelnerem. Skomplikowane słownictwo często maskuje brak zrozumienia.
Zadania Praktyczne
Przed rozmową przećwicz te scenariusze:
1. Zaprojektuj warstwę serwisów dla systemu e-commerce
Masz: UserService, ProductService, OrderService, PaymentService, NotificationService. Jakie zależności między nimi? Jak je wstrzyknąć? Co jeśli PaymentService może używać różnych bramek płatności?
2. Zrefaktoruj ten kod używając DI:
public class ReportGenerator {
public void generateReport() {
DatabaseConnection db = new MySqlConnection("localhost", "reports");
List<Data> data = db.query("SELECT * FROM sales");
PdfWriter writer = new PdfWriter();
writer.write(data, "report.pdf");
EmailSender sender = new SmtpEmailSender("smtp.company.com");
sender.send("manager@company.com", "report.pdf");
}
}
3. Zaimplementuj middleware pipeline w Express:
Stwórz: rate limiter (max 100 req/min per IP), auth middleware (JWT), request logger, error handler. Jaka kolejność? Jak obsłużyć błędy z async handlerów?
4. Wyjaśnij co się stanie i dlaczego:
app.get('/slow', async (req, res) => {
const result = crypto.pbkdf2Sync(req.query.password, 'salt', 1000000, 64, 'sha512');
res.send(result);
});
// Czy inne requesty będą obsługiwane podczas wykonywania /slow?
Zobacz też
- Jak Przygotować Się do Rozmowy z Node.js - szczegółowe pytania Node.js
- SQL na Rozmowie Rekrutacyjnej - bazy danych w backend
- Wzorce Projektowe w JavaScript - Pytania dla Seniorów - wzorce w JS
Rozwiń Swoje Umiejętności
Ten artykuł to zaledwie wierzchołek góry lodowej. Pełne przygotowanie do rozmowy o architekturę backend wymaga głębszego zrozumienia:
- Java: 150 pytań od podstaw OOP po zaawansowane multithreading i Stream API
- Spring: 56 pytań o IoC, AOP, Security, Data i mikroserwisy
- Node.js: 45 pytań o Event Loop, Streams, Cluster i wydajność
- Express: 49 pytań o middleware, routing, bezpieczeństwo i skalowanie
Uzyskaj dostęp do wszystkich pytań backend →
Lub wypróbuj bezpłatnie:
Artykuł powstał na podstawie doświadczeń z rozmów rekrutacyjnych w firmach technologicznych i ponad 10 lat praktyki w budowaniu systemów backend.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
