Java 25
Latest language features: records for every DTO, pattern matching, virtual-thread ready.
Languageseughost.tech is a fully anonymous campus board. No accounts to post, no author field in the database, and a small set of solid tools holding it together.
Latest language features: records for every DTO, pattern matching, virtual-thread ready.
LanguageWeb MVC, validation, caching and security wired together with sensible defaults.
FrameworkDocument store via Spring Data. Posts carry no author reference of any kind.
DatabaseServer-rendered pages with shared layout fragments, plus Spring Security tags for nav state.
TemplatingAdmin-only login for moderation. Everyone else posts and reads without an account.
SecurityBean Validation on request records, surfaced as a consistent JSON error body.
ValidationPage controllers for Thymeleaf views and REST controllers for the JSON API, with one shared error handler.
Web layerRepository interfaces over the document model, so queries stay short and typed.
Data accessIn-process cache on the public feed, evicted whenever a write could change what people see.
PerformanceNo framework. Small scripts use fetch to call /api/** for posting, reacting and moderation.
A hand-written theme with design tokens, glassy nav and soft cards. No Tailwind or Bootstrap.
StylingInter for the interface and Kalam for the handwritten feel of each note.
TypographyNo userId, hash or session on a post, so nothing can trace it back, even with database access.
Per-IP guards for posting, relating, flagging and views. Nothing persisted, no durable trail.
The public feed is cached in-process and evicted on every write that could change it.
Mockito unit tests, a minimal Spring context test for cache AOP, and validator boundary tests.
Controller โ Service โ Repository. Each layer has one job and only talks to the one below it.
Thymeleaf views, page controllers and model data kept apart, with a separate REST layer for the JSON API.
PostRepository and UserRepository hide all Mongo access behind plain interfaces.
Java records like PostResponse and PostCreateRequest mean documents never reach the web layer.
Constructor injection through Spring, with Lombok's @RequiredArgsConstructor keeping it tidy.
@Cacheable and @CacheEvict run on a Spring proxy: cache-aside for reads, eviction on writes.
User is created with Lombok's @Builder and @Builder.Default values.
Custom exceptions plus one @RestControllerAdvice give every API error the same JSON shape.
A @Scheduled job refreshes the feed cache every minute so view counts never look stuck.
Records for data carriers, enums like PostStatus and Role for typed states instead of strings.
Rate limiters use ConcurrentHashMap sets and deques so parallel requests stay correct.
Admin passwords are hashed with BCrypt, and UserService plugs into Spring Security as a UserDetailsService.
Hosts the app as a Docker container, deployed straight from git via a Render Blueprint.
HostingCloud-hosted MongoDB with two separate databases, one for development and one for production.
Cloud databaseSits in front of the site for DNS and fast, secure delivery to visitors.
DNS & EdgeHome at seughost.tech, a domain that fits a project built for a tech campus.
Domainreturn postRepository
.findByStatusOrderByPinnedDescCreatedAtDesc(PostStatus.APPROVED)
.stream()
.map(this::toPostResponse)
.toList();
public record PostCreateRequest(
@NotNull(message = "A post type is required.")
PostType type,
@NotBlank(message = "Thoughts can't be empty.")
@Size(min = 3, max = 1000)
String content,
// ...
) { }
public interface UserRepository
extends MongoRepository<User, String> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
Set<String> deduped = new LinkedHashSet<>();
for (String tag : tags) {
String trimmed = tag.trim();
if (!trimmed.isEmpty()) {
deduped.add(trimmed);
}
if (deduped.size() == MAX_TAGS) {
break;
}
}
return new ArrayList<>(deduped);
private final Map<String, ConcurrentLinkedDeque<Long>> postTimesByIp
= new ConcurrentHashMap<>();
long now = Instant.now().toEpochMilli();
ConcurrentLinkedDeque<Long> timestamps =
postTimesByIp.computeIfAbsent(ip,
key -> new ConcurrentLinkedDeque<>());
synchronized (timestamps) {
// prune old entries, check cooldown, record this post
}
private String clientIp(HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For");
if (forwardedFor != null && !forwardedFor.isBlank()) {
return forwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
public class AlreadyRelatedException
extends RuntimeException {
public AlreadyRelatedException(String message) {
super(message);
}
}
public enum PostStatus {
PENDING,
APPROVED,
REJECTED
}
// inside PostCreationRateLimiter
public enum Result { OK, COOLDOWN, DAILY_CAP }
@Controller
@RequiredArgsConstructor
public class HomeController {
private final PostService postService;
@GetMapping("/")
public String feed(Model model) { ... }
}
@ExtendWith(MockitoExtension.class)
class PostServiceTest {
@Mock private PostRepository postRepository;
@InjectMocks private PostService postService;
@Test
void createPost_nullType_throwsInvalidPostException() {
var request = new PostCreateRequest(null, null,
"Some thoughts.", null, List.of());
assertThatThrownBy(() -> postService.createPost(request))
.isInstanceOf(InvalidPostException.class);
}
}